From 662fd2e4065ff70f5a0139e63ae1fa954c7c9389 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Fri, 31 Jul 2026 00:24:57 +0000
Subject: [PATCH v2 v2 1/2] Fix crash in subscription REFRESH on concurrent
 relation drop.

check_publications_origin_tables() and
check_publications_origin_sequences() build a query that excludes
the subscription's local relations by name, so that
already-subscribed relations are not re-synchronized. They walk
the local relation OIDs without holding any lock and look up each
relation's schema and name. If a relation is dropped
concurrently, those lookups return InvalidOid and NULL, and the
NULL name is passed to quote_literal_cstr(), which dereferences
it unconditionally and crashes the backend. This can happen
during ALTER SUBSCRIPTION ... REFRESH PUBLICATION when a
subscribed table or sequence is dropped by another session.

Fix by opening each relation with try_table_open(), which returns
NULL if it is already gone, and skipping it in that case, as a
dropped relation is not synchronized anyway. The schema and name
are then read from the open relation instead of from unlocked
syscache lookups. The two functions carried an identical copy of
this loop, so it is factored out into a new helper,
append_quoted_rel_filters().

The table path may need to be backpatched down to 16 (commit
8756930190).

Author: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Co-authored-by: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reported-by: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Reviewed-by: Ajin Cherian <itsajin@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/CAHg%2BQDcd_o3707Ey8c8b7HkE-t14g8c0tk8ME3ctywDsh3ut8g%40mail.gmail.com
---
 src/backend/commands/subscriptioncmds.c | 74 ++++++++++++++-----------
 1 file changed, 42 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 013ac46db07..05d46fe05f3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -127,6 +127,8 @@ typedef struct PublicationRelKind
 } PublicationRelKind;
 
 static List *fetch_relation_list(WalReceiverConn *wrconn, List *publications);
+static void append_quoted_rel_filters(StringInfo cmd, Oid *relids,
+									  int nrelids);
 static void check_publications_origin_tables(WalReceiverConn *wrconn,
 											 List *publications, bool copydata,
 											 bool retain_dead_tuples,
@@ -3069,6 +3071,44 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
 	table_close(rel, RowExclusiveLock);
 }
 
+/*
+ * Append filter clauses to the origin-check query that exclude the given local
+ * relations, matching each by its schema-qualified name.
+ *
+ * The local relations are not locked, so a relation may be dropped
+ * concurrently. try_table_open() returns NULL if it is already gone, in which
+ * case it is skipped, as a dropped relation is not synchronized.
+ */
+static void
+append_quoted_rel_filters(StringInfo cmd, Oid *relids, int nrelids)
+{
+	for (int i = 0; i < nrelids; i++)
+	{
+		Relation	rel;
+		char	   *schemaname;
+		char	   *relname;
+		char	   *schemaname_lit;
+		char	   *relname_lit;
+
+		rel = try_table_open(relids[i], AccessShareLock);
+		if (rel == NULL)
+			continue;
+
+		schemaname = get_namespace_name(RelationGetNamespace(rel));
+		relname = RelationGetRelationName(rel);
+		schemaname_lit = quote_literal_cstr(schemaname);
+		relname_lit = quote_literal_cstr(relname);
+
+		appendStringInfo(cmd, "AND NOT (N.nspname = %s AND C.relname = %s)\n",
+						 schemaname_lit, relname_lit);
+
+		pfree(schemaname_lit);
+		pfree(relname_lit);
+
+		table_close(rel, AccessShareLock);
+	}
+}
+
 /*
  * Check and log a warning if the publisher has subscribed to the same table,
  * its partition ancestors (if it's a partition), or its partition children (if
@@ -3103,7 +3143,6 @@ check_publications_origin_tables(WalReceiverConn *wrconn, List *publications,
 	TupleTableSlot *slot;
 	Oid			tableRow[1] = {TEXTOID};
 	List	   *publist = NIL;
-	int			i;
 	bool		check_rdt;
 	bool		check_table_sync;
 	bool		origin_none = origin &&
@@ -3152,22 +3191,7 @@ check_publications_origin_tables(WalReceiverConn *wrconn, List *publications,
 	 * created subscriptions on the publisher.
 	 */
 	if (check_table_sync)
-	{
-		for (i = 0; i < subrel_count; i++)
-		{
-			Oid			relid = subrel_local_oids[i];
-			char	   *schemaname = get_namespace_name(get_rel_namespace(relid));
-			char	   *tablename = get_rel_name(relid);
-			char	   *schemaname_lit = quote_literal_cstr(schemaname);
-			char	   *tablename_lit = quote_literal_cstr(tablename);
-
-			appendStringInfo(&cmd, "AND NOT (N.nspname = %s AND C.relname = %s)\n",
-							 schemaname_lit, tablename_lit);
-
-			pfree(schemaname_lit);
-			pfree(tablename_lit);
-		}
-	}
+		append_quoted_rel_filters(&cmd, subrel_local_oids, subrel_count);
 
 	res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
 	pfree(cmd.data);
@@ -3280,21 +3304,7 @@ check_publications_origin_sequences(WalReceiverConn *wrconn, List *publications,
 	 * present on the subscriber. This check should be skipped as these will
 	 * not be re-synced.
 	 */
-	for (int i = 0; i < subrel_count; i++)
-	{
-		Oid			relid = subrel_local_oids[i];
-		char	   *schemaname = get_namespace_name(get_rel_namespace(relid));
-		char	   *seqname = get_rel_name(relid);
-		char	   *schemaname_lit = quote_literal_cstr(schemaname);
-		char	   *seqname_lit = quote_literal_cstr(seqname);
-
-		appendStringInfo(&cmd,
-						 "AND NOT (N.nspname = %s AND C.relname = %s)\n",
-						 schemaname_lit, seqname_lit);
-
-		pfree(schemaname_lit);
-		pfree(seqname_lit);
-	}
+	append_quoted_rel_filters(&cmd, subrel_local_oids, subrel_count);
 
 	res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
 	pfree(cmd.data);
-- 
2.47.3

