From 446c5e0d8c09dffb4b1090f0d205ffcb9c0f29a5 Mon Sep 17 00:00:00 2001
From: Dilip Kumar <dilipkumarb@google.com>
Date: Mon, 13 Jul 2026 16:19:00 +0530
Subject: [PATCH v2] Do not reassign subscriptions belonging to other databases
 in REASSIGN OWNED.

Subscription objects are fundamentally database-local in scope. Subscription
DDL commands already operate on a per-database basis: CREATE SUBSCRIPTION
stores subdbid = MyDatabaseId, and ALTER/DROP SUBSCRIPTION commands (including
ALTER SUBSCRIPTION ... OWNER TO) look subscriptions up by (MyDatabaseId, subname)
via the unique index on (subdbid, subname), acting strictly on subscriptions in
the current database. Furthermore, subscription fields like subserver (commit
8185bb5) and subconflictlogrelid (commit a5918fd) reference database-local
objects (!relisshared) in that database.

However, because pg_subscription is stored as a shared catalog table
(relisshared == true), shared dependency entries for subscriptions in
pg_shdepend have dbid = InvalidOid. As a result, REASSIGN OWNED attempted to
reassign subscriptions belonging to any database in the cluster. When
executed in a database other than subdbid, this caused
AlterSubscriptionOwner_internal() to resolve database-local references (such as
subserver and subconflictlogrelid) against the current database (MyDatabaseId),
resulting in errors or wrongly altering unrelated objects.

Fix this by skipping subscription ownership reassignment in
AlterSubscriptionOwner_internal() if form->subdbid != MyDatabaseId. Like other
database-local objects, subscriptions will now be reassigned when REASSIGN
OWNED is executed in the database to which they belong.
---
 doc/src/sgml/ref/reassign_owned.sgml    |  5 ++++-
 doc/src/sgml/user-manag.sgml            |  4 +++-
 src/backend/catalog/pg_subscription.c   | 13 +++++++++++++
 src/backend/commands/subscriptioncmds.c | 15 ++++++++++++++-
 4 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/reassign_owned.sgml b/doc/src/sgml/ref/reassign_owned.sgml
index ab692bd0690..5fca2329de2 100644
--- a/doc/src/sgml/ref/reassign_owned.sgml
+++ b/doc/src/sgml/ref/reassign_owned.sgml
@@ -48,7 +48,10 @@ REASSIGN OWNED BY { <replaceable class="parameter">old_role</replaceable> | CURR
       The name of a role. The ownership of all the objects within the
       current database, and of all shared objects (databases, tablespaces),
       owned by this role will be reassigned to
-      <replaceable class="parameter">new_role</replaceable>.
+      <replaceable class="parameter">new_role</replaceable>. Subscriptions,
+      although stored in a shared catalog, belong to a specific database
+      and are reassigned only when <command>REASSIGN OWNED</command> is executed
+      in that database.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 0ec32700bd4..f0b4ca069ec 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -530,7 +530,9 @@ ALTER TABLE bobs_table OWNER TO alice;
    that contains objects owned by the role.  (Note that the first
    such <command>REASSIGN OWNED</command> will change the ownership of any
    shared-across-databases objects, that is databases or tablespaces, that
-   are owned by the role-to-be-dropped.)
+   are owned by the role-to-be-dropped. Subscriptions, while stored in a
+   shared catalog, belong to a specific database and are reassigned only when
+   <command>REASSIGN OWNED</command> is executed in that database.)
   </para>
 
   <para>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 2068e03c571..387365f7ee4 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_type.h"
 #include "foreign/foreign.h"
 #include "miscadmin.h"
+#include "replication/logicallauncher.h"
 #include "storage/lmgr.h"
 #include "storage/lock.h"
 #include "utils/acl.h"
@@ -114,6 +115,12 @@ GetSubscription(Oid subid, bool missing_ok, bool conninfo_needed,
 
 	subform = (Form_pg_subscription) GETSTRUCT(tup);
 
+	/*
+	 * Outside the launcher process, subscription lookups must only access
+	 * subscriptions belonging to the current database.
+	 */
+	Assert(IsLogicalLauncher() || subform->subdbid == MyDatabaseId);
+
 	sub = palloc0_object(Subscription);
 	sub->cxt = cxt;
 	sub->oid = subid;
@@ -272,6 +279,9 @@ DisableSubscription(Oid subid)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for subscription %u", subid);
 
+	/* Must only modify subscriptions belonging to the current database. */
+	Assert(((Form_pg_subscription) GETSTRUCT(tup))->subdbid == MyDatabaseId);
+
 	LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock);
 
 	/* Form a new tuple. */
@@ -714,6 +724,9 @@ UpdateDeadTupleRetentionStatus(Oid subid, bool active)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for subscription %u", subid);
 
+	/* Must only modify subscriptions belonging to the current database. */
+	Assert(((Form_pg_subscription) GETSTRUCT(tup))->subdbid == MyDatabaseId);
+
 	LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock);
 
 	/* Form a new tuple. */
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 4292e7fb8f4..c40c5def1a3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -2868,6 +2868,9 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 
+	/* Must only alter subscriptions belonging to the current database. */
+	Assert(form->subdbid == MyDatabaseId);
+
 	if (form->subowner == newOwnerId)
 		return;
 
@@ -2987,6 +2990,7 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
 {
 	HeapTuple	tup;
 	Relation	rel;
+	Form_pg_subscription form;
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
@@ -2997,7 +3001,16 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("subscription with OID %u does not exist", subid)));
 
-	AlterSubscriptionOwner_internal(rel, tup, newOwnerId);
+	form = (Form_pg_subscription) GETSTRUCT(tup);
+
+	/*
+	 * Don't process subscriptions belonging to other databases. While
+	 * pg_subscription is a shared catalog, subscriptions refer to db-local
+	 * objects like subserver and subconflictlogrelid which exist only in the
+	 * database identified by subdbid.
+	 */
+	if (form->subdbid == MyDatabaseId)
+		AlterSubscriptionOwner_internal(rel, tup, newOwnerId);
 
 	heap_freetuple(tup);
 
-- 
2.49.0

