Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1whgdy-000Mfc-1n for pgsql-hackers@arkaria.postgresql.org; Thu, 09 Jul 2026 04:45:51 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1whgdw-00AqZm-2t for pgsql-hackers@arkaria.postgresql.org; Thu, 09 Jul 2026 04:45:49 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1whgdw-00AqZd-1X for pgsql-hackers@lists.postgresql.org; Thu, 09 Jul 2026 04:45:49 +0000 Received: from mail-oo1-xc33.google.com ([2607:f8b0:4864:20::c33]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1whgdu-00000000GN0-48ai for pgsql-hackers@lists.postgresql.org; Thu, 09 Jul 2026 04:45:48 +0000 Received: by mail-oo1-xc33.google.com with SMTP id 006d021491bc7-69eb8b6bea8so675905eaf.3 for ; Wed, 08 Jul 2026 21:45:47 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1783572347; cv=none; d=google.com; s=arc-20260327; b=HVm1A9R8WtIHEs6EjqGidIaxbralRtmD2ejs9pZOeefXE/xoomKyJHfpIy4wh6kKyQ QiY70b7J5zVPVxNLquGD+6QD6WN4ROOoWBDBNDHBpNzCbGu/JiAK6Bw7Fs1Tcy+0rhg5 0vaplH7ECaAsSyCI2jRtVk8OoU8mR+kGiZVm2zx22ha85GdxmcO4rcEJRY9Bk1jOSB+Y z00WL5QkE1wU1NPNHVdJ7m6BE0gqECzyCM5Uorm2H/uKd0h7cnQyWFMMlPNQGkAXXwAL ItH4tOYBeDU37srf0l3RGOEqaAlvoEBuC18B+RZ5pk/7bFyioWX7i7UZpu3cFPH2m6bg F2cw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20260327; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=B4tK5iC/sf6YyCxR/qPel1m0BuaFuMl1W176aGZQAI4=; fh=PyrCF+qsNaqndkAt6nzpU6jTKSl/iqI3Radtv0tyAIM=; b=q710GTPYBxtBdlr4TkmA3FVqTTlk33+rZH8woAuTbZMPWduDwxJd3hOlQSlRshcYdd CpLgzEVcdJo84iwsCRldw/xh+KQ2bWnoUlz7Chx+zIXeGf6zz0dskXRRfP6eoLobdM6D MLB9HvwupyYZpUHoGcPVoRQi/lM3VfUgpd8C7bOukt1kwKmrvBYgFlLYzY6SGbZ+CSt3 O30LHaVmyyN3o0jPeEJ1vuu6Okx/1vr4ys2kTuMU00hIkVmJUoaEmjUupmMj0fo1/TJd HDX56ao8LC4MMGEwRhKKKSch8mbjuHh/Qw0Ykz56LWDYVnqxU61keH+4lyEzLWNOyfCb m9jg==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1783572347; x=1784177147; darn=lists.postgresql.org; h=content-type:cc:to:subject:message-id:date:from:in-reply-to :references:mime-version:from:to:cc:subject:date:message-id:reply-to :content-type; bh=B4tK5iC/sf6YyCxR/qPel1m0BuaFuMl1W176aGZQAI4=; b=lzkh8e0buo05ZP5thDkGYUjE+nC2HldA0BFPB66yWr5+V9bIESFJ4b2GQXymW4+un0 YOpxMNYUIfPrpRGzhRkSqlwJr3ObyPR0D2Jz23nsyNv064cfFZle1Ao+Y/kSUoiUHdUz eGM3K5QC8x9BD0ItLcv15nu2YqUi6yUDPvBTFE4e6YbKbq8Hft0zgOyHBrXkRdRJEo2P 0BcjbEag3FvQs6vu39IOc13JCjHK001Y2NtV/6sU83cBYZNisPI3T2v/aaBN/9pJsnQs NEXCF+nwMJHfpOQi6kWvbcnaSfeO2VGiTFHUYIpfAwedaG3laaIZUrHmgycLl1S8cfN4 ZH7g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783572347; x=1784177147; h=content-type:cc:to:subject:message-id:date:from:in-reply-to :references:mime-version:x-gm-gg:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to:content-type; bh=B4tK5iC/sf6YyCxR/qPel1m0BuaFuMl1W176aGZQAI4=; b=Hzc+jUoQ0XVkmwJDarLaRsPg/sWXJEjTYpnhyscjo4m1bGXC7X9dDsutOTa4qaLjh3 R7i4V8nnJiWASfkQF7qlhqUdpjgRmaTMSvW7S69T4XXFw+Qb9U18TZTkoUZZQQamMLZo I+MqBqmskGhmY6vcyh5fGD43MqBvQYivR+5Und8E6F14H1Xq0n5MS1/mIs8nfpXQ84vb RccCWt3DdLkT0Ljd+9S/Orz7m/AIQpIOdQ9ls0cU7pqCKtrZVHnObJiXj9t09uNlFiGq aDmiKHX79hGuXpIlOcoNCPl3TDZj1xyMPqpWYUQsvaxUHuufyUavAb3+XElcJy73Jq1s /LfQ== X-Gm-Message-State: AOJu0YwDnE5MsG2yekS9idPxKreP0tTj43AhEMyP7JtEmayEi+DhNADr O0v1P5tiyukHDko00P6lHYOMvx97kFY9bV475lbvnQpeznW5UTHhPdzUJf8GnNppAAWLVk/IlIe Civ81ZYYCH8espk2ZRxqXljUss7xtLEo= X-Gm-Gg: AfdE7clSzdSjfQ2jx2MwOT4+O3cyZhUlzFt52pzyBKYfPcDKV+TQJ5lGarfDwqd1k5X lkDYVIpF+C+RLq5DV7h+ras+GwjNISnpkvXE7eymG83P6wK5CUl3fXVcKefSLYoL/GBXFgROTxQ +a7u7HYGbdp1EjflY4W9eHEswtG56MYPAYuyTUNOv5WOe9MYNWDYA4vgvSgStPM51JhBlH3a8Wa SZyGeN+YOTkCmdNUD7zaYRZ7PVIraNankXrbBWBjicqpKFfC/gRwAvZRc0XCz4ufl48fOzRPV7o 5oo= X-Received: by 2002:a05:6820:a0e:b0:6a3:7ea9:d64c with SMTP id 006d021491bc7-6a37eaa9a70mr901887eaf.63.1783572346480; Wed, 08 Jul 2026 21:45:46 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: From: surya poondla Date: Wed, 8 Jul 2026 21:45:34 -0700 X-Gm-Features: AUfX_mzVGJGJ68SEEHZKKg8GpfwGkNzveHHhAwG43nVTe7xwrzP4eNlVgSKpGOY Message-ID: Subject: Re: Fix races conditions in DropRole() and GrantRole() To: Bertrand Drouvot Cc: pgsql-hackers@lists.postgresql.org Content-Type: multipart/alternative; boundary="00000000000024aec30656264cec" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --00000000000024aec30656264cec Content-Type: text/plain; charset="UTF-8" Hi Bertrand, Thanks for the patch set. I agree the races are real, and reusing the RangeVarGetRelidExtended() invalidation-retry idiom is a good fit, the retry loop looks correct and closes the window for the covered paths. I have a few comments 1. The 0002 patch creates a new deadlock. GrantRole() now locks the member via roleSpecsToIds() (AccessShareLock, before the granted-role loop) and then the granted role via RoleNameGetOid() (ShareUpdateExclusiveLock). DROP ROLE takes AccessExclusiveLock per role in list order. Because AccessExclusiveLock conflicts with everything, this creates a lock-order cycle that did not exist before. For example say we have 2 concurrent sessions: S1: GRANT g TO m; S2: DROP ROLE g, m; 1. S1 acquires AccessShareLock(m) 2. S2 acquires AccessExclusiveLock(g) 3. S1 waits ShareUpdateExclusiveLock(g) -- blocked by S2 4. S2 waits AccessExclusiveLock(m) -- blocked by S1 -> deadlock Pre-patch, GRANT didn't lock the member, so S2 never blocked S1. The same expanded locking applies to CREATE ROLE ... ROLE and the ALTER ROLE ... ADD/DROP USER (ALTER GROUP) path, which also goes through roleSpecsToIds(). Swapping a silent orphan for a detected deadlock is arguably fine, but it's a behavior change, could you document the lock ordering, and maybe have DROP ROLE lock in a canonical (OID-sorted) order? 2. DROP ROLE now blocks on unrelated long transactions. The AccessShareLock from roleSpecsToIds() is held to commit, so an open txn that ran GRANT/CREATE ROLE ... ROLE/REASSIGN OWNED touching X, blocks a concurrent DROP ROLE X. This is intended behavior, but worth a note in the commit message/docs. 3. For non-cstring role specs, the else-branch (CURRENT_USER/SESSION_USER) does: roleid = get_rolespec_oid(rolespec, false); LockSharedObject(AuthIdRelationId, roleid, 0, AccessShareLock); get_rolespec_oid() returns the backend's cached session OID (GetUserId()) rather than re-resolving a name, so there is no retry mechanism and no post-lock existence check. Since DropRole() only blocks dropping the *dropping* session's own user, nothing stops another session from dropping this session's login role, so "GRANT g TO CURRENT_USER" can still orphan. RoleNameCallbackForDropRole() already does this by doing a re-check (a SearchSysCache1(AUTHOID) after resolving, erroring if the tuple is gone), so the else-branch could do the same after locking. 4. In role-membership-drop-member.spec only checks that the concurrent DROP waits; it never asserts the outcome, so it would still pass if the locking left an orphan. A final step selecting for orphaned rows would make it detect outcome regressions, e.g. SELECT m.roleid, m.member, m.grantor 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; Nit: the spec header comment has a stray '#' ("orphaned # pg_auth_members"). Regards, Surya Poondla --00000000000024aec30656264cec Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi Bertrand,


Thanks for = the patch set. I agree the races are real, and reusing the RangeVarGetRelid= Extended() invalidation-retry idiom is a good fit, the retry loop looks cor= rect and closes the window for the covered paths.

I have a few comme= nts
1. The 0002 patch creates a new deadlock. GrantRole() now loc= ks the member via roleSpecsToIds() (AccessShareLock, before the granted-rol= e loop) and then the granted role via RoleNameGetOid()
(ShareUpdateExclu= siveLock).
DROP ROLE takes AccessExclusiveLock per role in list o= rder. Because=C2=A0AccessExclusiveLock conflicts with everything, this crea= tes a lock-order cycle that did not exist before.=C2=A0
For examp= le say we have 2 concurrent sessions:
S1: GRANT g TO m; =C2=A0 = =C2=A0 =C2=A0 S2: DROP ROLE g, m;
1. S1 acquires AccessShareLock(m)=C2= =A0
2. S2 acquires AccessExclusiveLock(g)=C2=A0 =C2=A0
3. S1 waits Sh= areUpdateExclusiveLock(g) =C2=A0-- blocked by S2
4. S2 waits AccessExclu= siveLock(m)=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0-- blocked by S1
-> deadlock
Pre-patch, GR= ANT didn't lock the member, so S2 never blocked S1.=C2=A0
The same=C2=A0expanded locking applies to CREATE ROLE .= .. ROLE and the ALTER ROLE ... ADD/DROP USER (ALTER GROUP) path,=C2=A0
which also goes through roleSpecsToIds()= .=C2=A0
Swapp= ing a silent orphan for a detected deadlock is arguably fine, but it's = a behavior change, could you document the lock ordering, and maybe have DRO= P ROLE lock in a canonical (OID-sorted) order?

2.=C2=A0DROP ROLE now blocks on unrelated long transactions. The AccessS= hareLock=C2=A0from role= SpecsToIds() is held to commit,=C2=A0
so an open txn that ran GRANT/CREATE ROLE ... ROLE/REASSIGN OWNED touchin= g X, blocks a concurrent DROP ROLE X.=C2=A0=C2=A0This is intended behavior, but worth a note in th= e commit message/docs.

3. For non-cstring ro= le specs, the else-branch (CURRENT_USER/SESSION_USER)=C2=A0does:
roleid =3D get_rolespec_oid(rolespec, false)= ;
LockSharedObject(AuthIdRelationId, roleid, 0, AccessShareLock);
ge= t_rolespec_oid() returns the backend's cached session OID (GetUserId())= rather than re-resolving a name, so there is no retry mechanism and no=C2= =A0post-lock existence check.=C2=A0
Since DropRole() only blocks = dropping the *dropping* session's own user, nothing stops another sessi= on from dropping this session's login role, so "GRANT g TO CURRENT= _USER" can still orphan.=C2=A0
RoleNameCallbackForDropRole() alre= ady does this by doing a re-check (a SearchSysCache1(AUTHOID) after resolvi= ng, erroring if the tuple=C2=A0is gone), so the else-branch could do the same after locking.
<= div>
4. In=C2=A0role-membership-= drop-member.spec only checks that the concurrent DROP waits; it never asser= ts the outcome, so it would still pass if the locking left an orphan. A fin= al step selecting for orphaned rows would make it detect outcome regression= s, e.g.

=C2=A0 =C2=A0 =C2=A0 =C2=A0SELECT m.roleid, m.member,= m.grantor
=C2=A0 =C2=A0 =C2=A0 =C2=A0 FROM pg_auth_members m
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 LEFT JOIN pg_authid ra ON m.roleid =C2=A0=3D ra.oid=C2=A0 =C2=A0 =C2=A0 =C2=A0 LEFT JOIN pg_authid me ON m.member =C2=A0=3D m= e.oid
=C2=A0 =C2=A0 =C2=A0 =C2=A0 LEFT JOIN pg_authid gr ON m.grantor = =3D gr.oid
=C2=A0 =C2=A0 =C2=A0 =C2=A0WHERE ra.oid IS NULL OR me.oid IS = NULL OR gr.oid IS NULL;
Nit: the spec header comment has a stray '#' ("orphane= d # pg_auth_members").

Regards,
Surya Poondla
--00000000000024aec30656264cec--