From 221a61736769ba74a9c345a6c0978e14b6609532 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy 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 Co-authored-by: Bharath Rupireddy Reviewed-by: Ajin Cherian Reviewed-by: Masahiko Sawada 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