agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH v7 2/5] Add anytime flush tests for custom stats
35+ messages / 1 participants
[nested] [flat]

* [PATCH v7 2/5] Add anytime flush tests for custom stats
@ 2026-02-05 05:54  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-02-05 05:54 UTC (permalink / raw)

---
 .../test_custom_stats/t/001_custom_stats.pl   | 41 +++++++++++++
 .../test_custom_fixed_stats--1.0.sql          |  5 ++
 .../test_custom_fixed_stats.c                 | 57 +++++++++++++++++++
 .../test_custom_var_stats--1.0.sql            |  5 ++
 .../test_custom_stats/test_custom_var_stats.c | 27 +++++++++
 5 files changed, 135 insertions(+)
  33.8% src/test/modules/test_custom_stats/t/
  66.1% src/test/modules/test_custom_stats/

diff --git a/src/test/modules/test_custom_stats/t/001_custom_stats.pl b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
index 9e6a7a38577..7be1b281776 100644
--- a/src/test/modules/test_custom_stats/t/001_custom_stats.pl
+++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
@@ -156,5 +156,46 @@ $result = $node->safe_psql('postgres',
 );
 is($result, "0", "report of fixed-sized after manual reset");
 
+# Test FLUSH_ANYTIME mechanism with custom fixed stats
+# This verifies that custom stats can be flushed during a transaction
+
+# Reset stats first
+$node->safe_psql('postgres', q(select test_custom_stats_fixed_reset()));
+$node->safe_psql('postgres', q(select pg_stat_force_next_flush()));
+
+my $anytime_test = q[
+    BEGIN;
+    -- Accumulate stats
+    select test_custom_stats_fixed_anytime_update() from generate_series(1, 2);
+    -- Wait (has to be greater than PGSTAT_MIN_INTERVAL)
+    select pg_sleep(1.5);
+    -- Check
+    select 'anytime:'||numcalls from test_custom_stats_fixed_report();
+];
+
+$result = $node->safe_psql('postgres', $anytime_test);
+like($result, qr/^anytime:2/m,
+	"anytime fixed stats flushed during transaction");
+
+# Test FLUSH_ANYTIME mechanism with custom variable stats
+# This verifies that custom stats can be flushed during a transaction
+
+$node->safe_psql('postgres', q(select pg_stat_force_next_flush()));
+
+$anytime_test = q[
+    BEGIN;
+    -- Accumulate stats
+    select test_custom_stats_var_anytime_update('entry2');
+    select test_custom_stats_var_anytime_update('entry2');
+    -- Wait (has to be greater than PGSTAT_MIN_INTERVAL)
+    select pg_sleep(1.5);
+    -- Check
+	select * from test_custom_stats_var_report('entry2');
+];
+
+$result = $node->safe_psql('postgres', $anytime_test);
+like($result, qr/^entry2|2|/m,
+	"anytime var stats flushed during transaction");
+
 # Test completed successfully
 done_testing();
diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
index 69a93b5241f..da3a798f289 100644
--- a/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
+++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
@@ -18,3 +18,8 @@ CREATE FUNCTION test_custom_stats_fixed_reset()
 RETURNS void
 AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_reset'
 LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION test_custom_stats_fixed_anytime_update()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT PARALLEL UNSAFE;
diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.c b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c
index 908bd18a7c7..30b0fbcbdc7 100644
--- a/src/test/modules/test_custom_stats/test_custom_fixed_stats.c
+++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c
@@ -18,6 +18,7 @@
 #include "pgstat.h"
 #include "utils/builtins.h"
 #include "utils/pgstat_internal.h"
+#include "utils/timeout.h"
 
 PG_MODULE_MAGIC_EXT(
 					.name = "test_custom_fixed_stats",
@@ -43,11 +44,13 @@ typedef struct PgStatShared_CustomFixedEntry
 static void test_custom_stats_fixed_init_shmem_cb(void *stats);
 static void test_custom_stats_fixed_reset_all_cb(TimestampTz ts);
 static void test_custom_stats_fixed_snapshot_cb(void);
+static bool test_custom_stats_fixed_flush_cb(bool nowait, bool anytime_only);
 
 static const PgStat_KindInfo custom_stats = {
 	.name = "test_custom_fixed_stats",
 	.fixed_amount = true,		/* exactly one entry */
 	.write_to_file = true,		/* persist to stats file */
+	.flush_mode = FLUSH_ANYTIME,	/* can be flushed anytime */
 
 	.shared_size = sizeof(PgStat_StatCustomFixedEntry),
 	.shared_data_off = offsetof(PgStatShared_CustomFixedEntry, stats),
@@ -56,8 +59,12 @@ static const PgStat_KindInfo custom_stats = {
 	.init_shmem_cb = test_custom_stats_fixed_init_shmem_cb,
 	.reset_all_cb = test_custom_stats_fixed_reset_all_cb,
 	.snapshot_cb = test_custom_stats_fixed_snapshot_cb,
+	.flush_static_cb = test_custom_stats_fixed_flush_cb,
 };
 
+/* Pending statistics */
+static PgStat_StatCustomFixedEntry PendingCustomStats = {0};
+
 /*
  * Kind ID for test_custom_fixed_stats.
  */
@@ -141,6 +148,38 @@ test_custom_stats_fixed_snapshot_cb(void)
 #undef FIXED_COMP
 }
 
+/*
+ * test_custom_stats_fixed_flush_cb
+ *		Flush pending stats to shared memory
+ */
+static bool
+test_custom_stats_fixed_flush_cb(bool nowait, bool anytime_only)
+{
+	PgStatShared_CustomFixedEntry *stats_shmem;
+
+	/* Nothing to flush if no calls were made */
+	if (PendingCustomStats.numcalls == 0)
+		return false;
+
+	stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+
+	if (!nowait)
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+	else if (!LWLockConditionalAcquire(&stats_shmem->lock, LW_EXCLUSIVE))
+		return true;
+
+	pgstat_begin_changecount_write(&stats_shmem->changecount);
+	stats_shmem->stats.numcalls += PendingCustomStats.numcalls;
+	pgstat_end_changecount_write(&stats_shmem->changecount);
+
+	LWLockRelease(&stats_shmem->lock);
+
+	/* Reset pending stats */
+	PendingCustomStats.numcalls = 0;
+
+	return false;				/* successfully flushed */
+}
+
 /*--------------------------------------------------------------------------
  * SQL-callable functions
  *--------------------------------------------------------------------------
@@ -222,3 +261,21 @@ test_custom_stats_fixed_report(PG_FUNCTION_ARGS)
 	/* Return as tuple */
 	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
 }
+
+/*
+ * test_custom_stats_fixed_anytime_update
+ *		Increment call counter and schedule anytime flush
+ */
+PG_FUNCTION_INFO_V1(test_custom_stats_fixed_anytime_update);
+Datum
+test_custom_stats_fixed_anytime_update(PG_FUNCTION_ARGS)
+{
+	/* Accumulate in pending stats */
+	PendingCustomStats.numcalls++;
+
+	/* Schedule anytime stats update */
+	pgstat_schedule_anytime_update();
+	pgstat_report_fixed = true;
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
index 5ed8cfc2dcf..ed66d38981e 100644
--- a/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
@@ -24,3 +24,8 @@ CREATE FUNCTION test_custom_stats_var_report(INOUT name TEXT,
 RETURNS SETOF record
 AS 'MODULE_PATHNAME', 'test_custom_stats_var_report'
 LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION test_custom_stats_var_anytime_update(IN name TEXT)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_custom_stats_var_anytime_update'
+LANGUAGE C STRICT PARALLEL UNSAFE;
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c
index bc0b5d6e0eb..207e841911b 100644
--- a/src/test/modules/test_custom_stats/test_custom_var_stats.c
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c
@@ -17,6 +17,7 @@
 #include "storage/dsm_registry.h"
 #include "utils/builtins.h"
 #include "utils/pgstat_internal.h"
+#include "utils/timeout.h"
 
 PG_MODULE_MAGIC_EXT(
 					.name = "test_custom_var_stats",
@@ -107,6 +108,7 @@ static const PgStat_KindInfo custom_stats = {
 	.name = "test_custom_var_stats",
 	.fixed_amount = false,		/* variable number of entries */
 	.write_to_file = true,		/* persist across restarts */
+	.flush_mode = FLUSH_ANYTIME,	/* can be flushed anytime */
 	.track_entry_count = true,	/* count active entries */
 	.accessed_across_databases = true,	/* global statistics */
 	.shared_size = sizeof(PgStatShared_CustomVarEntry),
@@ -689,3 +691,28 @@ test_custom_stats_var_report(PG_FUNCTION_ARGS)
 
 	SRF_RETURN_DONE(funcctx);
 }
+
+/*
+ * test_custom_stats_var_anytime_update
+ *		Increment custom statistic counter and schedule anytime flush
+ */
+PG_FUNCTION_INFO_V1(test_custom_stats_var_anytime_update);
+Datum
+test_custom_stats_var_anytime_update(PG_FUNCTION_ARGS)
+{
+	char	   *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	PgStat_EntryRef *entry_ref;
+	PgStat_StatCustomVarEntry *pending_entry;
+
+	/* Get pending entry in local memory */
+	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid,
+										  PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), NULL);
+
+	pending_entry = (PgStat_StatCustomVarEntry *) entry_ref->pending;
+	pending_entry->numcalls++;
+
+	/* Schedule anytime stats update */
+	pgstat_schedule_anytime_update();
+
+	PG_RETURN_VOID();
+}
-- 
2.34.1


--C2xzVbxFmVrP7k6O
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v7-0003-Add-GUC-to-specify-non-transactional-statistics-f.patch"



^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--





^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v3 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

The AccessShareLock is held until end of transaction, so an open transaction
that performed GRANT, CREATE ROLE ... ROLE, or REASSIGN OWNED BY will block a
concurrent DROP ROLE on the same role until it commits.

Note that this introduces a potential deadlock between GRANT and DROP ROLE
when both target overlapping roles. The deadlock is detected and one session is
aborted with an error, which is preferable to the pre-patch behavior of silently
creating orphaned catalog entries.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by: Surya Poondla <suryapoondla4@gmail.com>
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 21 +++++-
 .../expected/role-membership-drop-member.out  | 75 +++++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 51 +++++++++++++
 4 files changed, 147 insertions(+), 1 deletion(-)
  14.9% src/backend/commands/
  48.1% src/test/isolation/expected/
  36.1% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..375fb527af2 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,24 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+
+			/* Recheck that the role still exists after locking. */
+			if (!SearchSysCacheExists1(AUTHOID, ObjectIdGetDatum(roleid)))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("role \"%s\" does not exist",
+								get_rolespec_name(rolespec))));
+		}
+
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..5b267ea32c2
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,75 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit s2_check_orphans
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+step s2_check_orphans: 
+	SELECT count(*)
+	FROM pg_auth_members m
+	LEFT JOIN pg_authid ra ON m.roleid  = ra.oid
+	LEFT JOIN pg_authid me ON m.member  = me.oid
+	LEFT JOIN pg_authid gr ON m.grantor = gr.oid
+	WHERE ra.oid IS NULL OR me.oid IS NULL OR gr.oid IS NULL;
+
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit s2_check_orphans
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+step s2_check_orphans: 
+	SELECT count(*)
+	FROM pg_auth_members m
+	LEFT JOIN pg_authid ra ON m.roleid  = ra.oid
+	LEFT JOIN pg_authid me ON m.member  = me.oid
+	LEFT JOIN pg_authid gr ON m.grantor = gr.oid
+	WHERE ra.oid IS NULL OR me.oid IS NULL OR gr.oid IS NULL;
+
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit s2_check_orphans
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+step s2_check_orphans: 
+	SELECT count(*)
+	FROM pg_auth_members m
+	LEFT JOIN pg_authid ra ON m.roleid  = ra.oid
+	LEFT JOIN pg_authid me ON m.member  = me.oid
+	LEFT JOIN pg_authid gr ON m.grantor = gr.oid
+	WHERE ra.oid IS NULL OR me.oid IS NULL OR gr.oid IS NULL;
+
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..989fe996249
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,51 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned pg_auth_members
+# entries, or from operating on a stale OID.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+step s2_check_orphans	{
+	SELECT count(*)
+	FROM pg_auth_members m
+	LEFT JOIN pg_authid ra ON m.roleid  = ra.oid
+	LEFT JOIN pg_authid me ON m.member  = me.oid
+	LEFT JOIN pg_authid gr ON m.grantor = gr.oid
+	WHERE ra.oid IS NULL OR me.oid IS NULL OR gr.oid IS NULL;
+}
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit s2_check_orphans
+
+# ALTER GROUP ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit s2_check_orphans
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit s2_check_orphans
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--Dvg/WemKV0I3T/Od--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread

* [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP
@ 2026-07-06 08:28  Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
  0 siblings, 0 replies; 35+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 08:28 UTC (permalink / raw)

roleSpecsToIds() resolves role names to OIDs without acquiring any lock.
A concurrent DROP ROLE that commits between this resolution and the caller's use
of the OID leaves the caller operating on a stale OID, which can create orphaned
pg_auth_members entries.

Fix this by acquiring AccessShareLock on each resolved role within
roleSpecsToIds(), ensuring the role cannot be dropped while any caller is using
its OID.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Virender Singla <virender.cse@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/aki6fMNLUx6%2BBR8K%40bdtpg
Discussion: https://postgr.es/m/CAM6Zo8woa62ZFHtMKox6a4jb8qQ%3Dw87R2L0K8347iE-juQL2EA%40mail.gmail.com
---
 src/backend/commands/user.c                   | 13 +++++-
 .../expected/role-membership-drop-member.out  | 36 ++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/role-membership-drop-member.spec    | 43 +++++++++++++++++++
 4 files changed, 92 insertions(+), 1 deletion(-)
  13.8% src/backend/commands/
  42.9% src/test/isolation/expected/
  42.2% src/test/isolation/specs/

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b869e91c17..05ec753f4f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1778,6 +1778,8 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
  * roleSpecsToIds
  *
  * Given a list of RoleSpecs, generate a list of role OIDs in the same order.
+ * Each role is locked with AccessShareLock to prevent concurrent DROP ROLE
+ * from removing it between resolution and the caller's catalog update.
  *
  * ROLESPEC_PUBLIC is not allowed.
  */
@@ -1792,7 +1794,16 @@ roleSpecsToIds(List *memberNames)
 		RoleSpec   *rolespec = lfirst_node(RoleSpec, l);
 		Oid			roleid;
 
-		roleid = get_rolespec_oid(rolespec, false);
+		if (rolespec->roletype == ROLESPEC_CSTRING)
+			roleid = RoleNameGetOid(rolespec->rolename,
+									AccessShareLock, false,
+									NULL, NULL);
+		else
+		{
+			roleid = get_rolespec_oid(rolespec, false);
+			LockSharedObject(AuthIdRelationId, roleid, 0,
+							 AccessShareLock);
+		}
 		result = lappend_oid(result, roleid);
 	}
 	return result;
diff --git a/src/test/isolation/expected/role-membership-drop-member.out b/src/test/isolation/expected/role-membership-drop-member.out
new file mode 100644
index 00000000000..e6cae59d2c9
--- /dev/null
+++ b/src/test/isolation/expected/role-membership-drop-member.out
@@ -0,0 +1,36 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_grant s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_grant: GRANT regress_role_group TO regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_alter_add s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_alter_add: ALTER GROUP regress_role_group ADD USER regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_create_role s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_create_role: CREATE ROLE regress_role_new ROLE regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_drop_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_drop_owned: DROP OWNED BY regress_role_member;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
+
+starting permutation: s1_begin s1_reassign_owned s2_drop_member s1_commit
+step s1_begin: BEGIN;
+step s1_reassign_owned: REASSIGN OWNED BY regress_role_member TO regress_role_group;
+step s2_drop_member: DROP ROLE regress_role_member; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_member: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..8fb8b52b77f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: role-membership-drop-member
diff --git a/src/test/isolation/specs/role-membership-drop-member.spec b/src/test/isolation/specs/role-membership-drop-member.spec
new file mode 100644
index 00000000000..e0140826e52
--- /dev/null
+++ b/src/test/isolation/specs/role-membership-drop-member.spec
@@ -0,0 +1,43 @@
+# Test that role membership commands properly lock the grantee/member
+# role to prevent concurrent DROP ROLE from creating orphaned # pg_auth_members
+# entries.
+
+setup
+{
+	CREATE ROLE regress_role_group;
+	CREATE ROLE regress_role_member;
+}
+
+teardown
+{
+	DROP ROLE IF EXISTS regress_role_group;
+	DROP ROLE IF EXISTS regress_role_member;
+	DROP ROLE IF EXISTS regress_role_new;
+}
+
+session s1
+step s1_begin		{ BEGIN; }
+step s1_grant		{ GRANT regress_role_group TO regress_role_member; }
+step s1_alter_add	{ ALTER GROUP regress_role_group ADD USER regress_role_member; }
+step s1_create_role	{ CREATE ROLE regress_role_new ROLE regress_role_member; }
+step s1_drop_owned	{ DROP OWNED BY regress_role_member; }
+step s1_reassign_owned	{ REASSIGN OWNED BY regress_role_member TO regress_role_group; }
+step s1_commit		{ COMMIT; }
+
+session s2
+step s2_drop_member	{ DROP ROLE regress_role_member; }
+
+# GRANT role TO member - concurrent DROP of the member
+permutation s1_begin s1_grant s2_drop_member s1_commit
+
+# ALTER ROLE ADD USER - concurrent DROP of the member
+permutation s1_begin s1_alter_add s2_drop_member s1_commit
+
+# CREATE ROLE ... ROLE member - concurrent DROP of the member
+permutation s1_begin s1_create_role s2_drop_member s1_commit
+
+# DROP OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_drop_owned s2_drop_member s1_commit
+
+# REASSIGN OWNED BY role - concurrent DROP of the role
+permutation s1_begin s1_reassign_owned s2_drop_member s1_commit
-- 
2.34.1


--cOHldBwZEf1OqVbN--






^ permalink  raw  reply  [nested|flat] 35+ messages in thread


end of thread, other threads:[~2026-07-06 08:28 UTC | newest]

Thread overview: 35+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-05 05:54 [PATCH v7 2/5] Add anytime flush tests for custom stats Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v3 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-07-06 08:28 [PATCH v2 2/2] Protect role resolution in roleSpecsToIds() against concurrent DROP Bertrand Drouvot <bertranddrouvot.pg@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox