From 221a61736769ba74a9c345a6c0978e14b6609532 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Wed, 19 Aug 2026 19:46:14 +0000
Subject: [PATCH v3] Fix crash in subscription REFRESH on concurrent relation
 drop.

ALTER SUBSCRIPTION ... REFRESH PUBLICATION looks up the schema
and name of each already-subscribed relation, without locking it,
to exclude it from the origin check. If such a relation is
dropped concurrently, the name lookups return NULL and the NULL
is passed to quote_literal_cstr(), which dereferences it and
crashes the backend.

Fix this by skipping the relation when its name is NULL, as a
dropped relation is not synchronized anyway.

Backpatch the fix for tables to 16 and for sequences to 19.

Author: Satya Narlapuram <satyanarlapuram@gmail.com>
Co-authored-by: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Ajin Cherian <itsajin@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/CAHg+QDcd_o3707Ey8c8b7HkE-t14g8c0tk8ME3ctywDsh3ut8g@mail.gmail.com
Backpatch-through: 16
---
 src/backend/commands/subscriptioncmds.c    | 44 ++++++++++++---
 src/test/subscription/t/001_rep_changes.pl | 63 ++++++++++++++++++++++
 2 files changed, 99 insertions(+), 8 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 6e805394808..b4eb0564e63 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -54,6 +54,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
@@ -1166,6 +1167,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 		subrel_states = GetSubscriptionRelations(sub->oid, true, true, false);
 		subrel_count = list_length(subrel_states);
 
+		/* Allow a test to drop a subscribed relation before the origin check. */
+		INJECTION_POINT("subscription-refresh-before-origin-check", NULL);
+
 		/*
 		 * Build qsorted arrays of local table oids and sequence oids for
 		 * faster lookup. This can potentially contain all tables and
@@ -3187,10 +3191,22 @@ check_publications_origin_tables(WalReceiverConn *wrconn, List *publications,
 		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);
+			char	   *schemaname;
+			char	   *tablename;
+			char	   *schemaname_lit;
+			char	   *tablename_lit;
+
+			/* The table may have been dropped concurrently; skip if gone. */
+			tablename = get_rel_name(relid);
+			if (tablename == NULL)
+				continue;
+
+			schemaname = get_namespace_name(get_rel_namespace(relid));
+			if (schemaname == NULL)
+				continue;
+
+			schemaname_lit = quote_literal_cstr(schemaname);
+			tablename_lit = quote_literal_cstr(tablename);
 
 			appendStringInfo(&cmd, "AND NOT (N.nspname = %s AND C.relname = %s)\n",
 							 schemaname_lit, tablename_lit);
@@ -3314,10 +3330,22 @@ check_publications_origin_sequences(WalReceiverConn *wrconn, List *publications,
 	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);
+		char	   *schemaname;
+		char	   *seqname;
+		char	   *schemaname_lit;
+		char	   *seqname_lit;
+
+		/* The sequence may have been dropped concurrently; skip if gone. */
+		seqname = get_rel_name(relid);
+		if (seqname == NULL)
+			continue;
+
+		schemaname = get_namespace_name(get_rel_namespace(relid));
+		if (schemaname == NULL)
+			continue;
+
+		schemaname_lit = quote_literal_cstr(schemaname);
+		seqname_lit = quote_literal_cstr(seqname);
 
 		appendStringInfo(&cmd,
 						 "AND NOT (N.nspname = %s AND C.relname = %s)\n",
diff --git a/src/test/subscription/t/001_rep_changes.pl b/src/test/subscription/t/001_rep_changes.pl
index 7d41715ed81..0fa4177c763 100644
--- a/src/test/subscription/t/001_rep_changes.pl
+++ b/src/test/subscription/t/001_rep_changes.pl
@@ -605,6 +605,69 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*) FROM pg_replication_origin");
 is($result, qq(0), 'check replication origin was dropped on subscriber');
 
+# Test that ALTER SUBSCRIPTION ... REFRESH PUBLICATION skips a subscribed
+# relation that is dropped concurrently during the refresh.
+SKIP:
+{
+	skip "injection points not supported by this build", 1
+	  if $node_subscriber->check_extension('injection_points') == 0;
+
+	# Subscribe to a table and a sequence.
+	$node_publisher->safe_psql(
+		'postgres', q{
+		CREATE TABLE tab_drop_refresh (a int);
+		CREATE SEQUENCE seq_drop_refresh;
+		CREATE PUBLICATION pub_drop_refresh FOR TABLE tab_drop_refresh;
+		CREATE PUBLICATION pub_seq_drop_refresh FOR ALL SEQUENCES;
+	});
+	$node_subscriber->safe_psql(
+		'postgres', qq{
+		CREATE EXTENSION IF NOT EXISTS injection_points;
+		CREATE TABLE tab_drop_refresh (a int);
+		CREATE SEQUENCE seq_drop_refresh;
+		CREATE SUBSCRIPTION sub_drop_refresh
+			CONNECTION '$publisher_connstr'
+			PUBLICATION pub_drop_refresh, pub_seq_drop_refresh
+			WITH (copy_data = false, origin = none);
+	});
+	$node_subscriber->wait_for_subscription_sync($node_publisher,
+		'sub_drop_refresh');
+
+	# Pause the refresh after it collects the relation list, drop the table
+	# and the sequence, then wake it.
+	$node_subscriber->safe_psql('postgres',
+		"SELECT injection_points_attach('subscription-refresh-before-origin-check', 'wait');"
+	);
+	my $bg = $node_subscriber->background_psql('postgres');
+	$bg->query_until(
+		qr/starting_refresh/, q{
+		\echo starting_refresh
+		ALTER SUBSCRIPTION sub_drop_refresh REFRESH PUBLICATION;
+	});
+	$node_subscriber->wait_for_event('client backend',
+		'subscription-refresh-before-origin-check');
+	$node_subscriber->safe_psql(
+		'postgres', q{
+		DROP TABLE tab_drop_refresh;
+		DROP SEQUENCE seq_drop_refresh;
+	});
+	$node_subscriber->safe_psql('postgres',
+		"SELECT injection_points_wakeup('subscription-refresh-before-origin-check');"
+	);
+	$bg->quit;
+
+	is($node_subscriber->safe_psql('postgres', 'SELECT 1;'),
+		'1', 'refresh survived a concurrently dropped table and sequence');
+
+	$node_subscriber->safe_psql(
+		'postgres', q{
+		SELECT injection_points_detach('subscription-refresh-before-origin-check');
+		DROP SUBSCRIPTION sub_drop_refresh;
+	});
+	$node_publisher->safe_psql('postgres',
+		'DROP PUBLICATION pub_drop_refresh, pub_seq_drop_refresh;');
+}
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.47.3

