From 8665017b0bbf42d867ebebe90a80724dfdb84847 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 16 Jun 2026 07:33:39 +0000
Subject: [PATCH v27 3/3] Add pg_attribute_aclcheck_ext to ACL tracking for
 dependency recording

Add an attnum field to AclCheckEntry so that recheckAcl() can distinguish
column-level checks from table-level checks and call the appropriate recheck
function. InvalidAttrNumber means whole-object check.

This also adds tracking to pg_attribute_aclcheck_ext(), so that future DDL
paths using column-level privileges will automatically get TOCTOU protection
without needing a pre-existing lock on the table.

Currently, the only caller of pg_attribute_aclcheck_ext() that also records
dependencies on the checked object is checkFkeyPermissions(), which holds
ShareRowExclusiveLock on the referenced table. While this prevents concurrent
REVOKE ON the table itself, it does not prevent concurrent role membership
revokes. Adding tracking here provides protection against that case and future
DDL callers.

The remaining aclcheck functions (pg_parameter_aclcheck and
pg_largeobject_aclcheck_snapshot) do not need tracking: pg_parameter_aclcheck
checks GUC parameters which are not dependency targets, and
pg_largeobject_aclcheck_snapshot is only called from inv_open() and
has_largeobject_privilege(), neither of which records dependencies.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/ZiYjn0eVc7pxVY45@ip-10-97-1-34.eu-west-3.compute.internal
---
 src/backend/catalog/aclchk.c         | 7 +++++--
 src/backend/catalog/pg_depend.c      | 7 ++++++-
 src/include/catalog/aclcheck_track.h | 8 ++++++--
 3 files changed, 17 insertions(+), 5 deletions(-)
  64.1% src/backend/catalog/
  35.8% src/include/catalog/

diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 02c4489f0c4..fb51ea2e869 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3899,7 +3899,7 @@ object_aclcheck_ext(Oid classid, Oid objectid,
 	if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
 						   is_missing) != 0)
 	{
-		aclcheck_track_record(classid, objectid, roleid, mode);
+		aclcheck_track_record(classid, objectid, InvalidAttrNumber, roleid, mode);
 		return ACLCHECK_OK;
 	}
 	else
@@ -3934,7 +3934,10 @@ pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
 {
 	if (pg_attribute_aclmask_ext(table_oid, attnum, roleid, mode,
 								 ACLMASK_ANY, is_missing) != 0)
+	{
+		aclcheck_track_record(RelationRelationId, table_oid, attnum, roleid, mode);
 		return ACLCHECK_OK;
+	}
 	else
 		return ACLCHECK_NO_PRIV;
 }
@@ -4104,7 +4107,7 @@ pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
 	if (pg_class_aclmask_ext(table_oid, roleid, mode,
 							 ACLMASK_ANY, is_missing) != 0)
 	{
-		aclcheck_track_record(RelationRelationId, table_oid, roleid, mode);
+		aclcheck_track_record(RelationRelationId, table_oid, InvalidAttrNumber, roleid, mode);
 		return ACLCHECK_OK;
 	}
 	else
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 912c92e8fef..88a27fdc802 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -773,7 +773,12 @@ recheckAcl(Oid classId, Oid objectId)
 		{
 			AclResult	aclresult;
 
-			if (classId == RelationRelationId)
+			if (acltable->entries[i].attnum != InvalidAttrNumber)
+				aclresult = pg_attribute_aclcheck(objectId,
+												  acltable->entries[i].attnum,
+												  acltable->entries[i].roleId,
+												  acltable->entries[i].mode);
+			else if (classId == RelationRelationId)
 				aclresult = pg_class_aclcheck(objectId,
 											  acltable->entries[i].roleId,
 											  acltable->entries[i].mode);
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
index d994cf7889b..4ac65e4de5c 100644
--- a/src/include/catalog/aclcheck_track.h
+++ b/src/include/catalog/aclcheck_track.h
@@ -30,6 +30,7 @@ typedef struct AclCheckEntry
 {
 	Oid			classId;
 	Oid			objectId;
+	AttrNumber	attnum;
 	Oid			roleId;
 	AclMode		mode;
 	uint64		inval_count;
@@ -50,11 +51,13 @@ extern void FreeTrackAclTable(TrackAclTable *acltable);
 /*
  * Record an aclcheck for later revalidation.
  *
- * Called from object_aclcheck_ext() and pg_class_aclcheck_ext().
+ * Called from object_aclcheck_ext(), pg_class_aclcheck_ext(), and
+ * pg_attribute_aclcheck_ext().
  * Only records when inside an utility statement.
  */
 static inline void
-aclcheck_track_record(Oid classId, Oid objectId, Oid roleId, AclMode mode)
+aclcheck_track_record(Oid classId, Oid objectId, AttrNumber attnum,
+					  Oid roleId, AclMode mode)
 {
 	TrackAclTable *acltable = CurrentTrackAclTable;
 	AclCheckEntry *entry;
@@ -72,6 +75,7 @@ aclcheck_track_record(Oid classId, Oid objectId, Oid roleId, AclMode mode)
 	entry = &acltable->entries[acltable->count++];
 	entry->classId = classId;
 	entry->objectId = objectId;
+	entry->attnum = attnum;
 	entry->roleId = roleId;
 	entry->mode = mode;
 	entry->inval_count = SharedInvalidMessageCounter;
-- 
2.34.1

