From a374857c5a255beaa5458e8d121388272a34821a Mon Sep 17 00:00:00 2001 From: Hayato Kuroda Date: Fri, 4 Sep 2026 12:35:21 +0900 Subject: [PATCH v6 1/2] pg_createsubscriber: ensure output_plugin_libraries include "pgoutput" pg_createsubscriber always uses pgoutput when creating logical replication slots. Check that pgoutput is included in the publisher's output_plugin_libraries setting before proceeding, and provide a useful error and hint when it is not. This also allows --dry-run to detect the invalid configuration before any replication slots are created. --- doc/src/sgml/ref/pg_createsubscriber.sgml | 3 + src/backend/utils/misc/guc_parameters.dat | 2 +- src/bin/pg_basebackup/pg_createsubscriber.c | 86 +++++++++++++++++++ .../t/040_pg_createsubscriber.pl | 48 +++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml index 193f60628bc..03f49991faf 100644 --- a/doc/src/sgml/ref/pg_createsubscriber.sgml +++ b/doc/src/sgml/ref/pg_createsubscriber.sgml @@ -431,6 +431,9 @@ PostgreSQL documentation databases plus existing replication slots. The source server must have configured to a value greater than or equal to the number of specified databases and existing WAL sender processes. + The source server must also allow the pgoutput output + plugin by setting to include + pgoutput. diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c57441f7d98..e0c8176c644 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2329,7 +2329,7 @@ { name => 'output_plugin_libraries', type => 'string', context => 'PGC_SUSET', group => 'REPLICATION_SENDING', short_desc => 'Lists libraries that may be named as logical decoding output plugins.', long_desc => 'Users with REPLICATION privileges may only use plugins in this list when creating logical replication slots.', - # Note that src/bin/pg_upgrade/check.c assumes GUC_LIST_QUOTE here. + # Note that src/bin/pg_upgrade/check.c and src/bin/pg_basebackup/pg_createsubscriber.c assume GUC_LIST_QUOTE here. flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', variable => 'output_plugin_libraries_string', boot_val => '"pgoutput, test_decoding"', diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c index 20b354aed56..2859fd7f4af 100644 --- a/src/bin/pg_basebackup/pg_createsubscriber.c +++ b/src/bin/pg_basebackup/pg_createsubscriber.c @@ -1006,6 +1006,89 @@ make_output_dirs(const char *log_basedir) pg_fatal("could not create directory \"%s\": %m", logdir); } +/* + * Check effective settings for each publisher database + */ +static void +check_publisher_per_database(const struct LogicalRepInfo *dbinfo) +{ + for (int i = 0; i < num_dbs; i++) + { + PGconn *conn; + PGresult *res; + char *output_plugin_libraries; + char *output_plugin_libraries_copy; + char **allowed_plugins; + bool pgoutput_allowed = false; + + conn = connect_database(dbinfo[i].pubconninfo, true); + + /* Check whether output_plugin_libraries includes 'pgoutput' */ + res = PQexec(conn, + "SELECT setting FROM pg_catalog.pg_settings " + "WHERE name = 'output_plugin_libraries'"); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + pg_log_error("could not obtain publisher settings in database \"%s\": %s", + dbinfo[i].dbname, PQresultErrorMessage(res)); + disconnect_database(conn, true); + } + + /* + * output_plugin_libraries is visible only for superuser or + * pg_read_all_settings role. Skip checking on this database if it + * cannot be read. + */ + if (PQntuples(res) != 1) + { + PQclear(res); + disconnect_database(conn, false); + continue; + } + + output_plugin_libraries = pg_strdup(PQgetvalue(res, 0, 0)); + + PQclear(res); + pg_log_debug("publisher in database \"%s\": output_plugin_libraries: %s", + dbinfo[i].dbname, output_plugin_libraries); + + disconnect_database(conn, false); + + output_plugin_libraries_copy = pg_strdup(output_plugin_libraries); + + if (!SplitGUCList(output_plugin_libraries_copy, ',', &allowed_plugins)) + { + /* Should not happen */ + pg_fatal("could not parse \"output_plugin_libraries\" setting '%s' in database \"%s\"", + output_plugin_libraries, dbinfo[i].dbname); + } + + /* Make sure the output_plugin_libraries setting includes "pgoutput" */ + for (char **plugin = allowed_plugins; *plugin; plugin++) + { + if (strcmp(*plugin, "pgoutput") == 0) + { + pgoutput_allowed = true; + break; + } + } + + if (!pgoutput_allowed) + { + pg_log_error("publisher does not allow the \"pgoutput\" output plugin in database \"%s\"", + dbinfo[i].dbname); + pg_log_error_hint("Add \"pgoutput\" to the configuration parameter \"%s\".", + "output_plugin_libraries"); + exit(1); + } + + pg_free(output_plugin_libraries); + pg_free(output_plugin_libraries_copy); + pg_free(allowed_plugins); + } +} + /* * Is the primary server ready for logical replication? * @@ -1137,6 +1220,9 @@ check_publisher(const struct LogicalRepInfo *dbinfo) if (failed) exit(1); + + /* Also check effective settings for each publisher database */ + check_publisher_per_database(dbinfo); } /* diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl index 9252d1c3c5c..e297236d8ad 100644 --- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl +++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl @@ -334,6 +334,54 @@ is($node_s->safe_psql($db1, "SELECT COUNT(*) FROM pg_publication"), $node_s->stop; +# pg_createsubscriber requires that output_plugin_libraries includes 'pgoutput' +# on the publisher. +$node_p->safe_psql($db2, + "ALTER DATABASE \"$db2\" SET output_plugin_libraries = 'test_decoding'"); + +command_fails_like( + [ + 'pg_createsubscriber', + '--dry-run', + '--pgdata' => $node_s->data_dir, + '--publisher-server' => $node_p->connstr($db1), + '--socketdir' => $node_s->host, + '--subscriber-port' => $node_s->port, + '--database' => $db1, + '--database' => $db2, + ], + qr/publisher does not allow the "pgoutput" output plugin in database "\Q$db2\E"/, + 'primary does not allow to load pgoutput plugin'); + +$node_p->safe_psql($db2, + "ALTER DATABASE \"$db2\" RESET output_plugin_libraries"); + +# output_plugin_libraries is invisible to non-superusers, so its check should +# be skipped even when pgoutput is not allowed. +my $publisher_user = 'regress_createsubscriber_user'; +$node_p->safe_psql('postgres', + "CREATE ROLE $publisher_user LOGIN REPLICATION"); +$node_p->safe_psql('postgres', + "ALTER ROLE $publisher_user SET output_plugin_libraries = 'test_decoding'"); +$result = $node_p->safe_psql($db1, + "SELECT count(*) FROM pg_catalog.pg_settings WHERE name = 'output_plugin_libraries'", + extra_params => [ '--username' => $publisher_user ]); +is($result, '0', 'non-superuser cannot see output_plugin_libraries'); + +command_ok( + [ + 'pg_createsubscriber', + '--dry-run', + '--pgdata' => $node_s->data_dir, + '--publisher-server' => $node_p->connstr($db1) . " user=$publisher_user", + '--socketdir' => $node_s->host, + '--subscriber-port' => $node_s->port, + '--database' => $db1, + ], + 'run pg_createsubscriber --dry-run as non-superuser'); + +$node_p->safe_psql('postgres', "DROP ROLE $publisher_user"); + # dry run mode on node S. Use the same publication name for different # databases, since publication names are database-local. command_ok( -- 2.52.0