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 1wfWpb-005V3U-2C for pgsql-hackers@arkaria.postgresql.org; Fri, 03 Jul 2026 05:52:55 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wfWpa-005fmw-1M for pgsql-hackers@arkaria.postgresql.org; Fri, 03 Jul 2026 05:52:54 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wfWpa-005fmn-0H for pgsql-hackers@lists.postgresql.org; Fri, 03 Jul 2026 05:52:54 +0000 Received: from mail-wr1-x434.google.com ([2a00:1450:4864:20::434]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wfWpU-00000001QkY-15pz for pgsql-hackers@lists.postgresql.org; Fri, 03 Jul 2026 05:52:53 +0000 Received: by mail-wr1-x434.google.com with SMTP id ffacd0b85a97d-47122683cf3so114090f8f.0 for ; Thu, 02 Jul 2026 22:52:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1783057967; x=1783662767; darn=lists.postgresql.org; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date:from:to :cc:subject:date:message-id:reply-to; bh=gUP7cv1uatupFdQrxS9TjGxSnLA4AvUCSXQJXd+gqDA=; b=n8vYnImCbqmLxG3Bp6wTnemdA9n2A2+R1Lg6e6GPOUW2PA0gur5tq51huFwByPLbm2 R+HLKuoWlZd1uUYtcSuc8g6ggqlb+yJ6QEeE3BtMMiu2Jxux5mVVPTwfyRtaloTRY7Nn eCjQ++Yyk21to6EYf/a3S9qSg6B2qzYCXbVgoozQAWpefScIkJiDO5Q2zH/sPj7V1tao 2ifuAUNBeSkAK2u9OOhfmTJ5EgfGpaOrQYLGJDPtwZ7BDvt+EQ76lArit75lxjXBYyI/ vjHopl1RpdUUKoM+37ea9qP4m8TtVahxP32/6tysNaE3Ua6HKiCGyqI6PCLjzNP/QBFJ lpig== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783057967; x=1783662767; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=gUP7cv1uatupFdQrxS9TjGxSnLA4AvUCSXQJXd+gqDA=; b=ShzGeYHhGQsYNgJz3CrJIoe0WZNsEiLKD2xoOazNLu4s3z1m9L0NavTArbk+2RzkdN oOKd5bK2d77ZpChXFc3D0WPzLrrIEV1KSsF6o3rE1pyfx63JdAmjPsy6BbOmL9KYkrwW TxVCaXgWITxVqGi5TwDgr36MvYwgUsFa6/fgx9+011UDMWMn+KcfCrpWIekMollqWcn0 3aW/qzIP+7Bb7Ul4ZdjG6sftUANBcjjpJRl/6VEMQgNZSAApM5ABcLwRTDigdY/+hf9n s8kATcmGQPKrPRCf3w1j7bc0847bJMCcB+UKHZa1xj5mjbBBluqNC6pXsvRyQ/QDkNWs WvHg== X-Forwarded-Encrypted: i=1; AHgh+RpToVGBoDW3hK+aUp4QdT0koDIo5GHSFHhOwfxffptofZr8Twounfg92g8kKhsx2djcb28+x8EE4QacwM7u@lists.postgresql.org X-Gm-Message-State: AOJu0YyJ+LA8wIX0pKPQgw3R6Mm0GmvJWAfxg74t9qCdUimuLyOU/KyR IcihdciR+nrvmZn1YLpuzK51I3Dsq/j/2CtJn+YIvRjAPtZmU8t4ezNhv1+kAg== X-Gm-Gg: AfdE7cmCmfX4AMsBYOFvTKPdcCEd650noGpmNRRZvnWfCcfqUymM+aFX//H8nh4ho+Q I9qO1PgUf3y3w9e2WbkXiCSCdLGr6W8wtisxeO3E/nPloD9T3GhGEkPFHuqixoTKAtNjIJpuIEb pa8XbO99LaEWHtpKy8tC8qZktx4EALkGZla8sHKpy067X7y8szIZphLQIBGkb+0aatGXm5nScbO 48uRbHL4qJ7l6iHNPtjgEt1C0mClSk7jF9kr2aFUwygiVWTFuMYF9N1iDprLBq5opSzNIs7Sl4X WJsN1SFnXnxf+C1gXb3PffWsY7qOqE88NHmK7KIoInkT2/nexJndwCp672fOWiNgnH830ab8snB kHXyB9tkCj2fJFrlrGDRLeZtbr0ds/w6Yq1vXh3RBpSAIeIoKT7liKi7QsprowKPPdwkMBcIqJt 3ieSgI/Xbc2iyfjbpDuEMg6nPXGn50By+/cp/d0FwEjgbXR2iFj3VHjMhKoStGnY8cs3CfVWLL X-Received: by 2002:a05:6000:27cb:10b0:460:71e6:e3b with SMTP id ffacd0b85a97d-47758db5955mr9418074f8f.27.1783057966716; Thu, 02 Jul 2026 22:52:46 -0700 (PDT) Received: from bdtpg (ec2-15-237-197-144.eu-west-3.compute.amazonaws.com. [15.237.197.144]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-477db8a4a09sm15416444f8f.13.2026.07.02.22.52.46 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 02 Jul 2026 22:52:46 -0700 (PDT) Date: Fri, 3 Jul 2026 05:52:44 +0000 From: Bertrand Drouvot To: Dilip Kumar Cc: "Hayato Kuroda (Fujitsu)" , "pgsql-hackers@lists.postgresql.org" Subject: Re: Re-read subscription state after lock in AlterSubscription Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="IX1d9TnmxjGKMYcM" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --IX1d9TnmxjGKMYcM Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit Hi, On Fri, Jul 03, 2026 at 10:20:32AM +0530, Dilip Kumar wrote: > On Fri, Jul 3, 2026 at 9:49 AM Bertrand Drouvot > wrote: > > > > Hi Kuroda-san, > > > > On Fri, Jul 03, 2026 at 03:13:08AM +0000, Hayato Kuroda (Fujitsu) wrote: > > > Dear Bertrand, > > > > > > > Yeah, but I think they would produce "tuple concurrently updated" error (due to > > > > CatalogTupleUpdate) so that invalid information could not be used. > > > > > > I confirmed with PG14 that tuple concurrently updated ERROR can be raised when > > > ALTER SUBSCRIPTION DISABLE happens concurrently: > > > > > > ``` > > > postgres=# ALTER SUBSCRIPTION sub DISABLE ; > > > ERROR: tuple concurrently updated > > > ``` > > > > Yeah, reproducible by using a breakpoint just before acquiring the lock for example. > > > > > It might be harmless but I think the correct ERROR should be reported: the patch > > > should be backpatched. Thought? > > > > I'm not sure about the back patch part as it would only improve error messages > > in a rare race condition (and there is no risk of invalid data being used). > > Patch LGTM. Thanks for looking at it! > IMHO we can backpatch this as it is a small change and > also fixes the bug, without this fix a non-superuser executing ALTER > SUBSCRIPTION could bypass the password_required=false restriction if a > concurrent transaction > updated that flag. I don't think that's right. I just tested it with a breakpoint that way: ALTER SUBSCRIPTION mysub SET (password_required = true); ALTER SUBSCRIPTION mysub OWNER TO nonsuperuser; gdb breakpoint at subscriptioncmds.c:1714 on session 1 (nonsuperuser) session 1 (as nonsuperuser): start ALTER SUBSCRIPTION mysub SET (binary = true); session 1 is paused by the breakpoint session 2 (as superuser): ALTER SUBSCRIPTION mysub SET (password_required = false); continue session 1, gives: postgres=> ALTER SUBSCRIPTION mysub SET (binary = true); ERROR: tuple concurrently updated So it's also "protected" by this error. > but given the patch's simplicity, I recommend > backpatching. That's right but that would only improve error messages. That said, looking closer, they are elog() ones, so "not expected" to occur so yeah backpatch does make sense. That said, what about also fixing DropSubscription() like in the 0002 attached? (that would also produce those elog() messages in case of concurrent DROP or ALTER). Regards, -- Bertrand Drouvot PostgreSQL Contributors Team RDS Open Source Databases Amazon Web Services: https://aws.amazon.com --IX1d9TnmxjGKMYcM Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0001-Re-read-subscription-state-after-lock-in-AlterSub.patch" From 483a9b0d6b0344cdb0562c76ef3a460a31125e96 Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Fri, 3 Jul 2026 05:17:31 +0000 Subject: [PATCH v2 1/2] Re-read subscription state after lock in AlterSubscription AlterSubscription() reads the subscription's catalog state via GetSubscription() before acquiring AccessExclusiveLock on the subscription object. A concurrent session that commits a DROP or ALTER between the read and the lock acquisition leaves the other session acting with stale information once it unblocks. Fix by: - Re-reading the subscription tuple after LockSharedObject() and refreshing the Subscription struct. - Moving the local variable assignments to after the re-read. - Re-checking the password_required privilege restriction after the re-read. Remarks: 1/ not re-checking password_required after the re-read would still produce a "tuple concurrently updated" error, but re-checking it allows us to display a better error message. 2/ the ownership check is intentionally not re-done after the lock because AlterSubscriptionOwner() does not take AccessExclusiveLock on the subscription object: it only takes RowExclusiveLock on the pg_subscription catalog table. This means ownership can change regardless of our lock, making a re-check after lock acquisition pointless. The existing "tuple concurrently updated" error from CatalogTupleUpdate() already provides a protection if ownership changes concurrently. Author: Bertrand Drouvot Reviewed-by: Dilip Kumar Reviewed-by: Hayato Kuroda (Fujitsu) Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg --- src/backend/commands/subscriptioncmds.c | 41 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) 100.0% src/backend/commands/ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 4292e7fb8f4..be03b3eb7e1 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -1695,11 +1695,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, */ sub = GetSubscription(subid, false, orig_conninfo_needed, false); - retain_dead_tuples = sub->retaindeadtuples; - origin = sub->origin; - max_retention = sub->maxretention; - retention_active = sub->retentionactive; - /* * Don't allow non-superuser modification of a subscription with * password_required=false. @@ -1713,6 +1708,42 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, /* Lock the subscription so nobody else can do anything with it. */ LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock); + /* + * Re-read the subscription tuple after acquiring the lock. A concurrent + * DROP or ALTER may have committed before we acquired the lock. + */ + heap_freetuple(tup); + tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), + CStringGetDatum(stmt->subname)); + + if (!HeapTupleIsValid(tup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("subscription \"%s\" does not exist", + stmt->subname))); + + form = (Form_pg_subscription) GETSTRUCT(tup); + + /* Refresh the subscription. */ + pfree(sub); + sub = GetSubscription(subid, false, orig_conninfo_needed, false); + + /* + * Re-check whether a non-superuser is allowed to alter this subscription. + * A concurrent ALTER may have set password_required=false while we were + * waiting for the lock. + */ + if (!sub->passwordrequired && !superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("password_required=false is superuser-only"), + errhint("Subscriptions with the password_required option set to false may only be created or modified by the superuser."))); + + retain_dead_tuples = sub->retaindeadtuples; + origin = sub->origin; + max_retention = sub->maxretention; + retention_active = sub->retentionactive; + /* Form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); -- 2.34.1 --IX1d9TnmxjGKMYcM Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0002-Re-read-subscription-state-after-lock-in-DropSubs.patch" From 6e357e922e48547ff9a07ea3e0fe5f69624207f0 Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Fri, 3 Jul 2026 05:18:44 +0000 Subject: [PATCH v2 2/2] Re-read subscription state after lock in DropSubscription Similarly to what has been done for AlterSubscription() in XXXX, re-read the subscription tuple after LockSharedObject() in DropSubscription(). A concurrent DROP or ALTER may have committed while we were waiting for the lock. Without a re-read, DropSubscription would deal with invalid data, which currently produces a confusing "tuple concurrently updated" elog() from CatalogTupleDelete(). Author: Bertrand Drouvot Reviewed-by: Reviewed-by: Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg --- src/backend/commands/subscriptioncmds.c | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) 100.0% src/backend/commands/ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index be03b3eb7e1..6db92a931b9 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -2582,17 +2582,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) return; } - datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo, &isnull); - if (!isnull) - subconninfo = TextDatumGetCString(datum); - form = (Form_pg_subscription) GETSTRUCT(tup); subid = form->oid; - subowner = form->subowner; - subserver = form->subserver; - subconflictlogrelid = form->subconflictlogrelid; - must_use_password = !superuser_arg(subowner) && form->subpasswordrequired; /* must be owner */ if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId())) @@ -2608,6 +2599,33 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) */ LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock); + /* + * Re-read the subscription tuple after acquiring the lock. A concurrent + * DROP or ALTER may have committed before we acquired the lock. + */ + ReleaseSysCache(tup); + tup = SearchSysCache2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), + CStringGetDatum(stmt->subname)); + + if (!HeapTupleIsValid(tup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("subscription \"%s\" does not exist", + stmt->subname))); + + form = (Form_pg_subscription) GETSTRUCT(tup); + subowner = form->subowner; + subserver = form->subserver; + subconflictlogrelid = form->subconflictlogrelid; + must_use_password = !superuser_arg(subowner) && form->subpasswordrequired; + + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo, &isnull); + if (!isnull) + subconninfo = TextDatumGetCString(datum); + else + subconninfo = NULL; + /* Get subname */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subname); -- 2.34.1 --IX1d9TnmxjGKMYcM--