postgres.git / summary / log / commit / refs

commit    5d47df21e89967e351df5ad7aa93bc3af40db64a
Author:   Jacob Champion <jchampion@postgresql.org>
Commit:   Noah Misch <noah@leadboat.com>
Date:     Mon Aug 10 13:38:05 2026 +0000

    Add an output_plugin_libraries GUC to bless trusted output plugins
    
    REPLICATION users were not previously subject to restrictions on output
    plugin paths, so they were able to bypass LOAD-time protections during
    logical decoding. Unfortunately, adding the standard LOAD restrictions
    now would retroactively require all third-party output plugins to be
    installed under the $libdir/plugins directory. This would prevent the
    use of dynamic_library_path, introduce a wire incompatibility for
    clients, and require all plugin authors to check that their libraries
    are safe for use by any unprivileged user; we want to avoid that.
    
    Instead, introduce an output_plugin_libraries GUC so that DBAs can
    specify the output plugins that are trusted for use in logical decoding.
    For simplicity, superusers are subject to the restriction as well
    (though they're free to modify the GUC at will during a session, so no
    power is actually lost).
    
    The default setting is 'pgoutput, test_decoding'. If other third-party
    plugins are in use, DBAs will need to modify this parameter after they
    update. Some pointers have been added to the documentation to assist
    with this.
    
    Author: Jacob Champion <jacob.champion@enterprisedb.com>
    Reported-by: Vladimir Tokarev <vladimirelitokarev@gmail.com>
    Reported-by: Yu Kunpeng <yu443940816@live.com>
    Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
    Reviewed-by: Noah Misch <noah@leadboat.com>
    Reviewed-by: Robert Haas <robertmhaas@gmail.com>
    Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
    Backpatch-through: 14
    Security: CVE-2026-6471


contrib/test_decoding/expected/permissions.out | 11 +++ contrib/test_decoding/expected/repack.out | 1 - contrib/test_decoding/expected/slot.out | 3 + contrib/test_decoding/sql/permissions.sql | 8 +++ contrib/test_decoding/sql/slot.sql | 3 + doc/src/sgml/config.sgml | 57 +++++++++++++++ doc/src/sgml/logical-replication.sgml | 13 +++- src/backend/replication/logical/logical.c | 97 ++++++++++++++++++++++++-- src/backend/replication/pgrepack/pgrepack.c | 10 ++- src/backend/utils/misc/guc_parameters.dat | 9 +++ src/backend/utils/misc/guc_tables.c | 1 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/bin/pg_dump/dumputils.c | 1 + src/bin/pg_upgrade/check.c | 85 +++++++++++++++++++++- src/bin/pg_upgrade/t/003_logical_slots.pl | 53 ++++++++++++-- src/include/replication/logical.h | 3 + src/test/subscription/t/100_bugs.pl | 27 ++++++- 17 files changed, 359 insertions(+), 24 deletions(-) diff --git a/contrib/test_decoding/expected/permissions.out b/contrib/test_decoding/expected/permissions.out index 8d100646ce6..6e94d7106bd 100644 --- a/contrib/test_decoding/expected/permissions.out +++ b/contrib/test_decoding/expected/permissions.out @@ -29,6 +29,17 @@ SELECT pg_drop_replication_slot('regression_slot'); (1 row) RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET output_plugin_libraries; -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/expected/repack.out b/contrib/test_decoding/expected/repack.out index c4ff41be690..5ddc63238c5 100644 --- a/contrib/test_decoding/expected/repack.out +++ b/contrib/test_decoding/expected/repack.out @@ -106,7 +106,6 @@ CREATE TABLE repack_plugin (a int); SELECT * FROM pg_create_logical_replication_slot('s_repack', 'pgrepack'); ERROR: unsupported use of logical decoding plugin "pgrepack" DETAIL: This plugin can only be used by REPACK (CONCURRENTLY). -CONTEXT: slot "s_repack", output plugin "pgrepack", in the startup callback INSERT INTO repack_plugin VALUES (1); SELECT * FROM pg_logical_slot_get_binary_changes('s_repack', NULL, NULL); ERROR: replication slot "s_repack" does not exist diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out index 7de03c79f6f..cf0136455b0 100644 --- a/contrib/test_decoding/expected/slot.out +++ b/contrib/test_decoding/expected/slot.out @@ -30,8 +30,11 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'tes init (1 row) +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); ERROR: could not access file "nonexistent": No such file or directory +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset \c - diff --git a/contrib/test_decoding/sql/permissions.sql b/contrib/test_decoding/sql/permissions.sql index 94db936aee2..b3d8b1a8e57 100644 --- a/contrib/test_decoding/sql/permissions.sql +++ b/contrib/test_decoding/sql/permissions.sql @@ -15,6 +15,14 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc SELECT pg_drop_replication_slot('regression_slot'); RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET output_plugin_libraries; + -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql index 580e3ae3bef..50a950341fc 100644 --- a/contrib/test_decoding/sql/slot.sql +++ b/contrib/test_decoding/sql/slot.sql @@ -9,7 +9,10 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_p', 'test SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'test_decoding', true); +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 1352ae85c10..88ed724b639 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4858,6 +4858,63 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows </listitem> </varlistentry> + <varlistentry id="guc-output-plugin-libraries" xreflabel="output_plugin_libraries"> + <term><varname>output_plugin_libraries</varname> (<type>string</type>) + <indexterm> + <primary><varname>output_plugin_libraries</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Lists the libraries installed in <xref linkend="guc-dynamic-library-path"/> + that are also trusted for use as logical output plugins by replication + clients. Any <link linkend="logicaldecoding-example">logical decoding</link> + or <link linkend="protocol-replication">replication</link> requests for + other libraries will be refused. All users are subject to this + restriction. The default is <literal>'pgoutput, test_decoding'</literal>, + which are the two logical output plugins included in the standard + <productname>PostgreSQL</productname> distribution. + </para> + <para> + The format is a comma-separated list of library names, where each name + is interpreted as for the <link linkend="sql-load"><command>LOAD</command></link> + command (but logical decoding clients must specify a plugin name that + <emphasis>exactly</emphasis> matches an entry in the list, without + variations in case or path structure). Whitespace between entries is + ignored; surround a library name with double quotes if you need to + include whitespace or commas in the name. + </para> + <para> + It is the responsibility of the server administrator to ensure that + libraries added to this list do not unintentionally give additional + privileges to non-superusers when they are loaded into the server. + </para> + <note> + <para> + When updating the server from a version that does not have the + <literal>output_plugin_libraries</literal> parameter, the following + query can help construct the list of plugins that are required by all + persistent logical replication slots: +<programlisting> +SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; +</programlisting> + Review the list carefully for safety before adjusting + <literal>output_plugin_libraries</literal>. + </para> + <para> + The above query can only display plugins which were successfully + added to replication slots at some point in the past. Newly refused + requests will appear in the logs with a message similar to +<programlisting> +ERROR: library "..." may not be used as an output plugin +DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +</programlisting> + </para> + </note> + </listitem> + </varlistentry> + <varlistentry id="guc-wal-keep-size" xreflabel="wal_keep_size"> <term><varname>wal_keep_size</varname> (<type>integer</type>) <indexterm> diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index efbed7b3bcb..938c939e97d 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2559,6 +2559,15 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER and it must have the <literal>LOGIN</literal> attribute. </para> + <para> + The name of the output plugin used by the replication connection must be + included in the server's <xref linkend="guc-output-plugin-libraries"/>. (For + subscriptions, the plugin name that is used is <literal>pgoutput</literal>.) + Superusers may modify the trusted list per-connection, by including + <literal>options=-coutput_plugin_libraries=...</literal> in the connection + string. + </para> + <para> In order to be able to copy the initial table data or synchronize sequences, the role used for the replication connection must have the @@ -2800,7 +2809,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER <listitem> <para> The output plugins referenced by the slots in the old cluster must be - installed in the new PostgreSQL executable directory. + installed in the new PostgreSQL executable directory. They must also be + included in the new cluster's <xref linkend="guc-output-plugin-libraries"/>; + see that parameter's documentation for safety information. </para> </listitem> <listitem> diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index c30d40a8641..98e5f1dd8f9 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -31,6 +31,7 @@ #include "access/xact.h" #include "access/xlog_internal.h" #include "access/xlogutils.h" +#include "commands/repack.h" #include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" @@ -42,9 +43,11 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/guc.h" #include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" +#include "utils/varlena.h" /* data for errcontext callback */ typedef struct LogicalErrorCallbackState @@ -54,6 +57,9 @@ typedef struct LogicalErrorCallbackState XLogRecPtr report_location; } LogicalErrorCallbackState; +/* GUC variables */ +char *output_plugin_libraries_string; + /* wrappers around output plugin callbacks */ static void output_plugin_error_callback(void *arg); static void startup_cb_wrapper(LogicalDecodingContext *ctx, OutputPluginOptions *opt, @@ -144,6 +150,7 @@ StartupDecodingContext(List *output_plugin_options, bool need_full_snapshot, bool fast_forward, bool in_create, + bool for_repack, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, LogicalOutputPluginWriterWrite do_write, @@ -170,7 +177,86 @@ StartupDecodingContext(List *output_plugin_options, * now. */ if (!fast_forward) - LoadOutputPlugin(&ctx->callbacks, NameStr(slot->data.plugin)); + { + /* + * Before loading this library, make sure it's been blessed for + * logical decoding. + */ + const char *plugin = NameStr(slot->data.plugin); + bool plugin_allowed = false; + + if (strcmp(plugin, "pgrepack") == 0) + { + /* + * REPACK is a special case -- print a more helpful error message + * here if we're not being called in the correct context. + */ + if (!for_repack || !AmRepackWorker()) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unsupported use of logical decoding plugin \"%s\"", + "pgrepack"), + errdetail("This plugin can only be used by %s.", + "REPACK (CONCURRENTLY)")); + + plugin_allowed = true; + } + else if (output_plugin_libraries_string && + output_plugin_libraries_string[0]) + { + /* Check this plugin against output_plugin_libraries. */ + char *rawstring; + List *elemlist = NIL; + + /* Need a modifiable copy */ + rawstring = pstrdup(output_plugin_libraries_string); + + if (!SplitGUCList(rawstring, ',', &elemlist)) + { + /* syntax error in list */ + ereport(LOG, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid list syntax in parameter \"%s\"", + "output_plugin_libraries"))); + + list_free(elemlist); + elemlist = NIL; + } + + foreach_ptr(char, allowed, elemlist) + { + if (strcmp(allowed, plugin) == 0) + { + plugin_allowed = true; + break; + } + } + + list_free(elemlist); + pfree(rawstring); + } + + if (!plugin_allowed) + { + /* + * Use the same error message as check_restricted_library_name(), + * but provide additional context for the DBA in the logs. (The + * HINT will be sent to the client, but that's not a secret.) + */ + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("library \"%s\" may not be used as an output plugin", + plugin), + /*- translator: The second %s is the value of the output_plugin_libraries GUC, which may contain whitespace, commas, and double-quotes (") */ + errdetail_log("The configuration parameter \"%s\" (currently '%s') does not name this library as a trusted output plugin.", + "output_plugin_libraries", + output_plugin_libraries_string), + errhint("If it is safe for all REPLICATION users to use this library as an output plugin, add it to \"%s\" and reload the server configuration.", + "output_plugin_libraries")); + } + + LoadOutputPlugin(&ctx->callbacks, plugin); + } /* * Now that the slot's xmin has been set, we can announce ourselves as a @@ -434,7 +520,7 @@ CreateInitDecodingContext(const char *plugin, ReplicationSlotSave(); ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon, - need_full_snapshot, false, true, + need_full_snapshot, false, true, for_repack, xl_routine, prepare_write, do_write, update_progress); @@ -569,7 +655,8 @@ CreateDecodingContext(XLogRecPtr start_lsn, ctx = StartupDecodingContext(output_plugin_options, start_lsn, InvalidTransactionId, false, - fast_forward, false, xl_routine, prepare_write, + fast_forward, false, false, + xl_routine, prepare_write, do_write, update_progress); /* call output plugin initialization callback */ @@ -720,7 +807,9 @@ OutputPluginUpdateProgress(struct LogicalDecodingContext *ctx, /* * Load the output plugin, lookup its output plugin init function, and check - * that it provides the required callbacks. + * that it provides the required callbacks. The caller must have checked that + * the current user has the necessary privileges to load the given plugin; + * standard LOAD restrictions are not applied here. */ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugin) diff --git a/src/backend/replication/pgrepack/pgrepack.c b/src/backend/replication/pgrepack/pgrepack.c index 5c5095bde4e..a1cbc8db68a 100644 --- a/src/backend/replication/pgrepack/pgrepack.c +++ b/src/backend/replication/pgrepack/pgrepack.c @@ -54,12 +54,10 @@ repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, RepackDecodingState *dstate; if (!AmRepackWorker()) - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("unsupported use of logical decoding plugin \"%s\"", - "pgrepack"), - errdetail("This plugin can only be used by %s.", - "REPACK (CONCURRENTLY)")); + { + /* StartupDecodingContext() should have caught this case already */ + elog(FATAL, "unexpected pgrepack startup outside of repack worker"); + } /* Initial setup of our private state */ Assert(CurrentMemoryContext == ctx->context); diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 2cd115deaee..15f9261dab6 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2308,6 +2308,15 @@ ifdef => 'DEBUG_BOUNDED_SORT', }, +{ 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. + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'output_plugin_libraries_string', + boot_val => '"pgoutput, test_decoding"', +}, + { name => 'parallel_leader_participation', type => 'bool', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', short_desc => 'Controls whether Gather and Gather Merge also run subplans.', long_desc => 'Should gather nodes also run subplans or just gather tuples?', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 290ccbc543e..7fd83081462 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -72,6 +72,7 @@ #include "postmaster/syslogger.h" #include "postmaster/walsummarizer.h" #include "postmaster/walwriter.h" +#include "replication/logical.h" #include "replication/logicallauncher.h" #include "replication/slot.h" #include "replication/slotsync.h" diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 23dd27957be..f61bd6ad4de 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -863,6 +863,7 @@ #dynamic_library_path = '$libdir' #extension_control_path = '$system' #gin_fuzzy_search_limit = 0 +#output_plugin_libraries = 'pgoutput, test_decoding' # approved plugins for logical decoding #------------------------------------------------------------------------------ diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 0d9f817875d..a08afaf8de5 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -734,6 +734,7 @@ variable_is_guc_list_quote(const char *name) { if (pg_strcasecmp(name, "local_preload_libraries") == 0 || pg_strcasecmp(name, "oauth_validator_libraries") == 0 || + pg_strcasecmp(name, "output_plugin_libraries") == 0 || pg_strcasecmp(name, "search_path") == 0 || pg_strcasecmp(name, "session_preload_libraries") == 0 || pg_strcasecmp(name, "shared_preload_libraries") == 0 || diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 184379c52af..e298e5658da 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -2210,6 +2210,7 @@ check_new_cluster_replication_slots(void) int nslots_on_new; int rdt_slot_on_new; int max_replication_slots; + char *output_plugin_libraries; char *wal_level; int i_nslots_on_new; int i_rdt_slot_on_new; @@ -2269,10 +2270,10 @@ check_new_cluster_replication_slots(void) PQclear(res); res = executeQueryOrDie(conn, "SELECT setting FROM pg_settings " - "WHERE name IN ('wal_level', 'max_replication_slots') " + "WHERE name IN ('wal_level', 'output_plugin_libraries', 'max_replication_slots') " "ORDER BY name DESC;"); - if (PQntuples(res) != 2) + if (PQntuples(res) != 3) pg_fatal("could not determine parameter settings on new cluster"); wal_level = PQgetvalue(res, 0, 0); @@ -2282,7 +2283,85 @@ check_new_cluster_replication_slots(void) pg_fatal("\"wal_level\" must be \"replica\" or \"logical\" but is set to \"%s\"", wal_level); - max_replication_slots = atoi(PQgetvalue(res, 1, 0)); + output_plugin_libraries = PQgetvalue(res, 1, 0); + + /* + * Make sure the output_plugin_libraries setting covers all plugins needed + * by any migrated slots. + */ + if (nslots_on_old > 0) + { + char *guc_copy = pg_strdup(output_plugin_libraries); + char **allowed_plugins; + char output_path[MAXPGPATH]; + FILE *script = NULL; + + if (!SplitGUCList(guc_copy, ',', &allowed_plugins)) + { + /* + * Should not happen. (Frontend and backend GUC_LIST_QUOTE parsing + * have to remain compatible for pg_dump at minimum.) + */ + pg_fatal("could not parse \"output_plugin_libraries\" setting '%s'", + output_plugin_libraries); + } + + snprintf(output_path, sizeof(output_path), "%s/%s", + log_opts.basedir, + "disallowed_output_plugins.txt"); + + for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++) + { + LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr; + + for (int slotnum = 0; slotnum < slot_arr->nslots; slotnum++) + { + LogicalSlotInfo *slot = &slot_arr->slots[slotnum]; + bool allowed = false; + + /* + * We expect the output_plugin_libraries length to be small in + * practice; O(n*m) shouldn't be a problem here. + */ + for (char **p = allowed_plugins; *p; p++) + { + if (strcmp(slot->plugin, *p) == 0) + { + allowed = true; + break; + } + } + + if (!allowed) + { + if (script == NULL && + (script = fopen_priv(output_path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", output_path); + + fprintf(script, "The slot \"%s\" uses plugin \"%s\"\n", + slot->slotname, slot->plugin); + } + } + } + + if (script) + { + fclose(script); + + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains logical replication slots with plugins\n" + "that are not allowed by the new cluster's output_plugin_libraries\n" + "setting. You can add trusted plugins to output_plugin_libraries\n" + "and/or remove affected slots, and then restart the upgrade.\n" + "A list of the problematic slots is in the file:\n" + " %s", output_path); + } + + pg_free(allowed_plugins); + pg_free(guc_copy); + } + + max_replication_slots = atoi(PQgetvalue(res, 2, 0)); if (old_cluster.sub_retain_dead_tuples && nslots_on_old + 1 > max_replication_slots) diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl index 05918aad935..01ab82402ae 100644 --- a/src/bin/pg_upgrade/t/003_logical_slots.pl +++ b/src/bin/pg_upgrade/t/003_logical_slots.pl @@ -62,7 +62,7 @@ chdir ${PostgreSQL::Test::Utils::tmp_check}; $oldpub->start; $oldpub->safe_psql( 'postgres', qq[ - SELECT pg_create_logical_replication_slot('test_slot1', 'test_decoding'); + SELECT pg_create_logical_replication_slot('test_slot1', 'pgoutput'); SELECT pg_create_logical_replication_slot('test_slot2', 'test_decoding'); SELECT pg_create_logical_replication_slot('test_slot3', 'test_decoding'); ]); @@ -90,6 +90,52 @@ ok(-d $newpub->data_dir . "/pg_upgrade_output.d", # old cluster. Both slots will be used for subsequent tests. $newpub->append_conf('postgresql.conf', "max_replication_slots = 3"); +# ------------------------------ +# TEST: Confirm pg_upgrade fails when slot plugins are prohibited by the new cluster + +$newpub->append_conf('postgresql.conf', + "output_plugin_libraries = 'pgoutput'"); + +command_checks_all( + [@pg_upgrade_cmd], + 1, + [ + qr/Your installation contains logical replication slots with plugins/, + qr/that are not allowed by the new cluster's output_plugin_libraries/, + ], + [qr//], + 'run of pg_upgrade where the old cluster has untrusted output plugins'); + +my $slots_filename; + +# Find a txt file that contains a list of logical replication slots that cannot +# be upgraded. We cannot predict the file's path because the output directory +# contains a milliseconds timestamp. File::Find::find must be used. +find( + sub { + if ($File::Find::name =~ m/disallowed_output_plugins\.txt/) + { + $slots_filename = $File::Find::name; + } + }, + $newpub->data_dir . "/pg_upgrade_output.d"); + +# Check the report. +my $content = slurp_file($slots_filename); +like( + $content, + qr/The slot "test_slot2" uses plugin "test_decoding"/m, + 'the previous test failed due to prohibited plugins'); +like( + $content, + qr/The slot "test_slot3" uses plugin "test_decoding"/m, + 'the previous test failed due to prohibited plugins'); +unlike($content, qr/test_slot1/m, 'allowed plugin is not reported'); + +# Fix things for the next tests. +$newpub->append_conf('postgresql.conf', + "output_plugin_libraries = 'pgoutput, test_decoding'"); + # ------------------------------ # TEST: Confirm pg_upgrade fails when the slot still has unconsumed WAL records @@ -127,11 +173,6 @@ command_checks_all( ); # Verify the reason why the logical replication slot cannot be upgraded -my $slots_filename; - -# Find a txt file that contains a list of logical replication slots that cannot -# be upgraded. We cannot predict the file's path because the output directory -# contains a milliseconds timestamp. File::Find::find must be used. find( sub { if ($File::Find::name =~ m/invalid_logical_slots\.txt/) diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index 6e0b7628001..abc88c49ade 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -154,6 +154,9 @@ extern XLogRecPtr LogicalReplicationSlotCheckPendingWal(XLogRecPtr end_of_wal, extern XLogRecPtr LogicalSlotAdvanceAndCheckSnapState(XLogRecPtr moveto, bool *found_consistent_snapshot); +/* GUCs */ +extern PGDLLIMPORT char *output_plugin_libraries_string; + /* * This macro determines the log level for messages about starting logical diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 075c52f98fd..335efd86bca 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -477,13 +477,13 @@ $result = is( $result, qq(2|f 3|t), 'check replicated update on subscriber'); -# Test create and immediate drop of replication slot via replication commands -# (this exposed a memory-management bug in v18) my $publisher_host = $node_publisher->host; my $publisher_port = $node_publisher->port; my $connstr_db = "host=$publisher_host port=$publisher_port replication=database dbname=postgres"; +# Test create and immediate drop of replication slot via replication commands +# (this exposed a memory-management bug in v18) is( $node_publisher->psql( 'postgres', qq[ @@ -495,6 +495,27 @@ is( $node_publisher->psql( 0, 'create and immediate drop of replication slot'); +# REPLICATION users should not be able to bypass LOAD restrictions. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE USER repluser REPLICATION; +)); + +my ($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', + qq[ + SET ROLE repluser; + CREATE_REPLICATION_SLOT fail_slot LOGICAL regress;', + ], + timeout => $PostgreSQL::Test::Utils::timeout_default, + extra_params => [ '-d', $connstr_db ]); + +is($ret, 3, 'loading unblessed output plugin fails'); +like( + $stderr, + qr/ERROR: library "regress" may not be used as an output plugin/, + 'loading unblessed output plugin fails: stderr'); + $node_publisher->stop('fast'); $node_subscriber->stop('fast'); @@ -592,7 +613,7 @@ $node_publisher->safe_psql( CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub WITH (connect=false); )); -my ($ret, $stdout, $stderr) = +($ret, $stdout, $stderr) = $node_publisher->psql('postgres', q{DROP SUBSCRIPTION regress_sub1}); isnt($ret, 0, "replication slot does not exist: exit code not 0"); [parent: 7082ce248a52]