From 58259ae107537ef8a7a0ac9bc7a5bc9ab5ad0ab9 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 29 Dec 2023 16:13:38 +0100
Subject: [PATCH v3 4/4] Add support to change GUCs at the protocol level

Currently the only way to set GUCs from a client is by using SET
commands or set them in the StartupMessage. This adds a new ParameterSet
protocol message, that can be used to re-configure protocol extensions
after the StartupMessage. This is useful, because clients and connection
poolers don't want protocol specific parameters to change without them
knowing, so allowing changes through a regular SET command is not an
option. Changing protocol extension parameters using this new message is
non-transactional, but it is possible to be run in a pipeline as long as
no transaction is open.

Finally, this also adds a new protocol extension that can be used to
"upgrade" regular GUCs to protcol extensions. This makes it possible
for a connection pooler to take ownership of SESSION AUTHORIZATION or
ROLE. That way it can set SESSION AUTHORIZATION on a superuser
connection to the user that the client should be connected as, without
it being possible for the client to change SESSION AUTHORIZATION back.
Because the client won't be able to sneak past a SET SESSION
AUTHORIZATION command in SQL, and if it would send a ParamaterSet
command itself then the pooler could simply disallow that.
---
 doc/src/sgml/config.sgml                      |  49 ++++
 doc/src/sgml/libpq.sgml                       |  62 +++++
 doc/src/sgml/protocol.sgml                    |  98 +++++++
 src/backend/postmaster/postmaster.c           |   8 +-
 src/backend/tcop/postgres.c                   |  26 ++
 src/backend/utils/misc/guc.c                  | 117 +++++++-
 src/backend/utils/misc/guc_tables.c           |  13 +
 src/include/libpq/protocol.h                  |   2 +
 src/include/utils/guc.h                       |   6 +
 src/include/utils/guc_hooks.h                 |   2 +
 src/include/utils/guc_tables.h                |   1 +
 src/interfaces/libpq/exports.txt              |   2 +
 src/interfaces/libpq/fe-connect.c             |   4 +
 src/interfaces/libpq/fe-exec.c                |  76 ++++++
 src/interfaces/libpq/fe-protocol3.c           |  25 ++
 src/interfaces/libpq/fe-trace.c               |  21 ++
 src/interfaces/libpq/libpq-fe.h               |   3 +
 src/interfaces/libpq/libpq-int.h              |   4 +-
 .../modules/libpq_pipeline/libpq_pipeline.c   | 254 ++++++++++++++++++
 .../libpq_pipeline/t/001_libpq_pipeline.pl    |   2 +-
 .../libpq_pipeline/traces/parameter_set.trace | 126 +++++++++
 21 files changed, 893 insertions(+), 8 deletions(-)
 create mode 100644 src/test/modules/libpq_pipeline/traces/parameter_set.trace

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018f..e3bf5b15e0b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10977,6 +10977,55 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
     </variablelist>
    </sect1>
 
+   <sect1 id="runtime-config-protocol">
+    <title>Protocol Behaviour</title>
+    <para>
+     There are several parameters that control the behaviour of the protocol,
+     these are called protocol extension parameters. These types of parameters
+     are different from all other runtime parameters in a few important ways.
+     First, they all start with a <literal>_pq_.</literal> prefix. Secondly,
+     they can only be set at the protocol level, either using the
+     StartupMessage or using a ParamaterSet message. It's not possible to set
+     them in any other way (not through command line arguments, configuration
+     files, SET, etc). Finally, if you configure one of them in the
+     StartupMessage, but the server doesn't support the parameter the
+     connection attempt does not fail like with other options. Instead the
+     connection attempt continues as normal as these protocol parameters are
+     considered optional to implement by the server. To check if the parameter
+     was set you need to check the return value of
+     <xref linkend="libpq-PQunsupportedProtocolExtensions"/>. If one of the
+     requested parameters was not supported by the server it will be listed
+     there.
+    </para>
+
+     <variablelist>
+
+     <varlistentry id="guc-pq-protocol-managed-params" xreflabel="_pq_.protocol_managed_params">
+      <term><varname>_pq_.protocol_managed_params</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>_pq_.protocol_managed_params</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This parameter can be used to "upgrade" regular runtime parameters to a
+        protocol extension parameter. Thus disallowing it to be set in any
+        other way than through the StartupMessage or ParameterSet protocol
+        messages. This can be useful to limit the power of an attacker with
+        arbitrary SQL execution. For example, if you set
+        <literal>_pq_.protocol_managed_params</literal> to
+        <literal>session_authorization</literal> then you can connect as a
+        highly privileged user to <productname>PostgreSQL</productname> but
+        change session_authorization to a user with fewer privileges. And then
+        the attacker with only SQL access (but not protocol access) is unable
+        to change back the session authorization.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     </variablelist>
+   </sect1>
+
    <sect1 id="runtime-config-custom">
     <title>Customized Options</title>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 72b66295f69..dc78d98f03e 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2212,6 +2212,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-pq-protocol-managed-params" xreflabel="load_balance_hosts">
+      <term><literal>_pq_.protocol_managed_params</literal></term>
+      <listitem>
+       <para>
+        Specifies a value for the <xref linkend="guc-pq-protocol-managed-params"/>
+        configuration parameter.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -3436,6 +3446,37 @@ PGresult *PQclosePortal(PGconn *conn, const char *portalName);
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-PQparameterSet">
+      <term><function>PQparameterSet</function><indexterm><primary>PQparameterSet</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Submits a request to change a protocol parameter, and waits for completion.
+<synopsis>
+PGresult *PQparameterSet(PGconn *conn, const char *parameter, const char *value);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQparameterSet"/> allows a client to change
+        protocol extension parameters on the current connection. Protocol
+        parameters start with <literal>_pq_.</literal>, or should be listed in
+        <xref linkend="guc-pq-protocol-managed-params"/>. Making changes to
+        these parameters using this function is non transactional, and this
+        function will return an error if it's called while a transaction is
+        active (or aborted).
+       </para>
+
+       <para>
+        <parameter>parameter</parameter> is the name of the parameter to
+        change, and <parameter>value</parameter> is the value to which to
+        change it. On success, a <structname>PGresult</structname> with status
+        <literal>PGRES_COMMAND_OK</literal> is returned.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
    </para>
 
@@ -5128,6 +5169,27 @@ int PQsendClosePortal(PGconn *conn, const char *portalName);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendParameterSet">
+     <term><function>PQsendParameterSet</function><indexterm><primary>PQsendParameterSet</primary></indexterm></term>
+
+     <listitem>
+      <para>
+        Submits a request to change a protocol parameter, without waiting for
+        completion.
+<synopsis>
+int PQsendParameterSet(PGconn *conn, const char *portalName);
+</synopsis>
+
+       This is an asynchronous version of <xref linkend="libpq-PQparameterSet"/>:
+       it returns 1 if it was able to dispatch the request, and 0 if not.
+       After a successful call, call <xref linkend="libpq-PQgetResult"/> to
+       obtain the results.  The function's parameters are handled
+       identically to <xref linkend="libpq-PQparameterSet"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+
     <varlistentry id="libpq-PQgetResult">
      <term><function>PQgetResult</function><indexterm><primary>PQgetResult</primary></indexterm></term>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d7..1a43a90616e 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1368,6 +1368,36 @@ SELCT 1/0;<!-- this typo is intentional -->
    </note>
   </sect2>
 
+  <sect2 id="protocol-changing-backend-parameters">
+   <title>Changing backend parameters</title>
+   <para>
+    The ParameterSet message can be used to change the value of a protocol
+    extension parameter. It's not allowed to change protocol extension
+    parameters using a SET command. So this message is essentially the SET
+    equivalent for protocol extension parameters. There are a few other
+    differences between SET and ParameterSet. First, ParameterSet is
+    not allowed to be sent while a transaction is in progress.
+    Second, the ParameterSet message is
+    easier for connection poolers to intercept. Finally, using the
+    _pq_.protocol_managed_params protocol extension it's possible to change a
+    normal parameter to a protocol extension parameter. Thus allowing it not to
+    be modified using SET anymore.
+   </para>
+
+   <note>
+    <para>
+     While ParameterSet is not itself part of any transaction, it is possible
+     to execute it as part of a pipeline, but only if no transaction has been
+     started. For most usecases this effectively means that ParameterSet
+     messages need to be the only (or at least the first) messages in a
+     pipeline because any regular query will open an implicit transaction.
+     Just like with other messages in a pipeline, if an error occurs while
+     processing ParameterSet, the following messages in the pipeline are
+     ignored. Regular processing only continues when a Sync is received.
+    </para>
+   </note>
+  </sect2>
+
   <sect2 id="protocol-flow-canceling-requests">
    <title>Canceling Requests in Progress</title>
 
@@ -5271,6 +5301,74 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ParameterSet">
+    <term>ParameterSet (F)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('U')</term>
+       <listitem>
+        <para>
+         Identifies the message as a run-time parameter change.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the run-time parameter to change.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The new value of the parameter.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ParameterSetComplete">
+    <term>ParameterSetComplete (B)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('U')</term>
+       <listitem>
+        <para>
+         Identifies the message as a ParamaterSet-complete indicator.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32(4)</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-Parse">
     <term>Parse (F)</term>
     <listitem>
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index fb04e4dde31..118e45e1549 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -124,6 +124,7 @@
 #include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datetime.h"
+#include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/pidfile.h"
 #include "utils/ps_status.h"
@@ -2210,12 +2211,11 @@ retry1:
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
-			else if (strncmp(nameptr, "_pq_.", 5) == 0)
+			else if (strncmp(nameptr, "_pq_.", 5) == 0 && !find_option(nameptr, false, true, ERROR))
 			{
 				/*
-				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * We report unkown protocol extensions using the
+				 * NegotiateProtocolVersion message instead of erroring
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d18..b612ad64068 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -427,6 +427,7 @@ SocketBackend(StringInfo inBuf)
 		case PqMsg_Describe:
 		case PqMsg_Execute:
 		case PqMsg_Flush:
+		case PqMsg_ParameterSet:
 			maxmsglen = PQ_SMALL_MESSAGE_LIMIT;
 			doing_extended_query_message = true;
 			break;
@@ -4851,6 +4852,31 @@ PostgresMain(const char *dbname, const char *username)
 				send_ready_for_query = true;
 				break;
 
+			case PqMsg_ParameterSet:
+				{
+					const char *parameter_name;
+					const char *parameter_value;
+
+					forbidden_in_wal_sender(firstchar);
+					if (IsTransactionOrTransactionBlock())
+						ereport(ERROR,
+								(errcode(ERRCODE_PROTOCOL_VIOLATION),
+								 errmsg("ParameterSet message is not allowed within a transaction")));
+
+					parameter_name = pq_getmsgstring(&input_message);
+					parameter_value = pq_getmsgstring(&input_message);
+					pq_getmsgend(&input_message);
+
+					SetConfigOption(
+									parameter_name,
+									parameter_value,
+									PGC_USERSET,
+									PGC_S_PROTOCOL);
+					if (whereToSendOutput == DestRemote)
+						pq_putemptymessage(PqMsg_ParameterSetComplete);
+				}
+				break;
+
 				/*
 				 * 'X' means that the frontend is closing down the socket. EOF
 				 * means unexpected loss of frontend connection. Either way,
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d929a179a86..d9c3f6ee7a6 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,9 +46,12 @@
 #include "utils/builtins.h"
 #include "utils/conffiles.h"
 #include "utils/float.h"
+#include "utils/guc.h"
+#include "utils/guc_hooks.h"
 #include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
+#include "utils/varlena.h"
 
 
 #define CONFIG_FILENAME "postgresql.conf"
@@ -2011,7 +2014,7 @@ ResetAllOptions(void)
 			gconf->context != PGC_USERSET)
 			continue;
 		/* Don't reset if special exclusion from RESET ALL */
-		if (gconf->flags & GUC_NO_RESET_ALL)
+		if (gconf->flags & (GUC_NO_RESET_ALL | GUC_PROTOCOL_EXTENSION))
 			continue;
 		/* No need to reset if wasn't SET */
 		if (gconf->source <= PGC_S_OVERRIDE)
@@ -3573,6 +3576,24 @@ set_config_with_handle(const char *name, config_handle *handle,
 			break;
 	}
 
+	if (record->flags & GUC_PROTOCOL_EXTENSION)
+	{
+		if (source != PGC_S_CLIENT && source != PGC_S_PROTOCOL)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("parameter can only be set at the protocol level \"%s\"", name)));
+			return 0;
+		}
+	}
+	else if (source == PGC_S_PROTOCOL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("parameter cannot be set at the protocol level \"%s\"", name)));
+		return 0;
+	}
+
 	/*
 	 * Disallow changing GUC_NOT_WHILE_SEC_REST values if we are inside a
 	 * security restriction context.  We can reject this regardless of the GUC
@@ -4606,7 +4627,8 @@ AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt)
 			 */
 			if ((record->context == PGC_INTERNAL) ||
 				(record->flags & GUC_DISALLOW_IN_FILE) ||
-				(record->flags & GUC_DISALLOW_IN_AUTO_FILE))
+				(record->flags & GUC_DISALLOW_IN_AUTO_FILE) ||
+				(record->flags & GUC_PROTOCOL_EXTENSION))
 				ereport(ERROR,
 						(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
 						 errmsg("parameter \"%s\" cannot be changed",
@@ -6905,3 +6927,94 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+
+/*
+ * GUC check_hook for protocol_managed_params
+ */
+bool
+check_protocol_managed_params(char **newval, void **extra, GucSource source)
+{
+	List	   *namelist;
+	ListCell   *cell;
+	char	   *protocol_params_str = pstrdup(*newval);
+
+	if (!SplitIdentifierString(protocol_params_str, ',', &namelist))
+	{
+		/* syntax error in name list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(protocol_params_str);
+		list_free(namelist);
+		return false;
+	}
+
+	foreach(cell, namelist)
+	{
+		char	   *pname = (char *) lfirst(cell);
+
+		if (!find_option(pname, false, true, ERROR))
+		{
+			GUC_check_errdetail("Parameter \"%s\" is not recognized.", pname);
+			pfree(protocol_params_str);
+			list_free(namelist);
+			return false;
+		}
+
+		if (strncmp(pname, "_pq_.", 5) == 0)
+		{
+			GUC_check_errdetail("Parameter \"%s\" is a protocol extension.", pname);
+			pfree(protocol_params_str);
+			list_free(namelist);
+			return false;
+		}
+	}
+
+	pfree(protocol_params_str);
+	list_free(namelist);
+	return true;
+}
+
+
+/*
+ * GUC check_hook for protocol_managed_params
+ */
+void
+assign_protocol_managed_params(const char *newval, void *extra)
+{
+	List	   *namelist;
+	ListCell   *cell;
+	char	   *old_protocol_params_str = pstrdup(protocol_managed_params);
+	char	   *protocol_params_str = pstrdup(newval);
+
+	if (!SplitIdentifierString(old_protocol_params_str, ',', &namelist))
+	{
+		elog(ERROR, "List syntax is invalid and check hook should have checked.");
+	}
+
+	foreach(cell, namelist)
+	{
+		char	   *pname = (char *) lfirst(cell);
+		struct config_generic *config = find_option(pname, false, false, ERROR);
+
+		config->flags &= ~GUC_PROTOCOL_EXTENSION;
+	}
+
+	list_free(namelist);
+
+	if (!SplitIdentifierString(protocol_params_str, ',', &namelist))
+	{
+		elog(ERROR, "List syntax is invalid and check hook should have checked.");
+	}
+
+	foreach(cell, namelist)
+	{
+		char	   *pname = (char *) lfirst(cell);
+		struct config_generic *config = find_option(pname, false, false, ERROR);
+
+		config->flags |= GUC_PROTOCOL_EXTENSION;
+	}
+
+	pfree(old_protocol_params_str);
+	pfree(protocol_params_str);
+	list_free(namelist);
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92dddd..73acff68a22 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -492,6 +492,8 @@ extern const struct config_enum_entry dynamic_shared_memory_options[];
 /*
  * GUC option variables that are exported from this module
  */
+char	   *protocol_managed_params = "";
+
 bool		log_duration = false;
 bool		Debug_print_plan = false;
 bool		Debug_print_parse = false;
@@ -657,6 +659,7 @@ const char *const GucSource_Names[] =
 	 /* PGC_S_CLIENT */ "client",
 	 /* PGC_S_OVERRIDE */ "override",
 	 /* PGC_S_INTERACTIVE */ "interactive",
+	 /* PGC_S_PROTOCOL */ "protocol",
 	 /* PGC_S_TEST */ "test",
 	 /* PGC_S_SESSION */ "session"
 };
@@ -3854,6 +3857,16 @@ struct config_real ConfigureNamesReal[] =
 
 struct config_string ConfigureNamesString[] =
 {
+	{
+		{"_pq_.protocol_managed_params", PGC_USERSET, PROTOCOL_EXTENSION,
+			gettext_noop("List of additional parameters to be only managed at the protocol level."),
+			NULL,
+			GUC_PROTOCOL_EXTENSION | GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE
+		},
+		&protocol_managed_params,
+		"",
+		check_protocol_managed_params, assign_protocol_managed_params, NULL
+	},
 	{
 		{"archive_command", PGC_SIGHUP, WAL_ARCHIVING,
 			gettext_noop("Sets the shell command that will be called to archive a WAL file."),
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index cc46f4b586a..bdbd1356da8 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -25,6 +25,7 @@
 #define PqMsg_Parse					'P'
 #define PqMsg_Query					'Q'
 #define PqMsg_Sync					'S'
+#define PqMsg_ParameterSet			'U'
 #define PqMsg_Terminate				'X'
 #define PqMsg_CopyFail				'f'
 #define PqMsg_GSSResponse			'p'
@@ -52,6 +53,7 @@
 #define PqMsg_RowDescription		'T'
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
+#define PqMsg_ParameterSetComplete	'U'
 #define PqMsg_ReadyForQuery			'Z'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 631c09c16b2..98c1e4945b7 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -118,6 +118,7 @@ typedef enum
 	PGC_S_CLIENT,				/* from client connection request */
 	PGC_S_OVERRIDE,				/* special case to forcibly set default */
 	PGC_S_INTERACTIVE,			/* dividing line for error reporting */
+	PGC_S_PROTOCOL,				/* from a ParameterSet message */
 	PGC_S_TEST,					/* test per-database or per-user setting */
 	PGC_S_SESSION,				/* SET command */
 } GucSource;
@@ -223,6 +224,9 @@ typedef enum
 #define GUC_DISALLOW_IN_AUTO_FILE \
 							   0x002000 /* can't set in PG_AUTOCONF_FILENAME */
 #define GUC_RUNTIME_COMPUTED   0x004000 /* delay processing in 'postgres -C' */
+#define GUC_PROTOCOL_EXTENSION   \
+							   0x008000 /* only allowed to be set using
+										 * ParameterSet and StartupMessage */
 
 #define GUC_UNIT_KB			 0x01000000 /* value is in kilobytes */
 #define GUC_UNIT_BLOCKS		 0x02000000 /* value is in blocks */
@@ -240,6 +244,8 @@ typedef enum
 
 
 /* GUC vars that are actually defined in guc_tables.c, rather than elsewhere */
+extern PGDLLIMPORT char *protocol_managed_params;
+
 extern PGDLLIMPORT bool Debug_print_plan;
 extern PGDLLIMPORT bool Debug_print_parse;
 extern PGDLLIMPORT bool Debug_print_rewritten;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 3d74483f447..3f741cbfc0a 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -25,6 +25,8 @@
  * Please keep the declarations in order by GUC variable name.
  */
 
+extern bool check_protocol_managed_params(char **newval, void **extra, GucSource source);
+extern void assign_protocol_managed_params(const char *newval, void *extra);
 extern bool check_application_name(char **newval, void **extra,
 								   GucSource source);
 extern void assign_application_name(const char *newval, void *extra);
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index eaa8c46ddac..a4df8fdcfde 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -99,6 +99,7 @@ enum config_group
 	PRESET_OPTIONS,
 	CUSTOM_OPTIONS,
 	DEVELOPER_OPTIONS,
+	PROTOCOL_EXTENSION,
 };
 
 /*
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 849617cb9b2..9df8281965a 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -192,3 +192,5 @@ PQclosePortal             189
 PQsendClosePrepared       190
 PQsendClosePortal         191
 PQunsupportedProtocolExtensions 192
+PQparameterSet            193
+PQsendParameterSet        194
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 8a1ca07ab3b..c669bda84b3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -359,6 +359,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
 	offsetof(struct pg_conn, load_balance_hosts)},
 
+	{"_pq_.protocol_managed_params", NULL, NULL, NULL,
+		"Pq-Protocol-Managed-Params", "", 40,
+	offsetof(struct pg_conn, pq_protocol_managed_params)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index b9511df2c26..a4aa223c345 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -3346,6 +3346,82 @@ PQsendFlushRequest(PGconn *conn)
 	return 1;
 }
 
+/*
+ * PQparameterSet
+ *	  Send a request for the server to change a run-time parameter setting.
+ *
+ * If the query was not even sent, return NULL; conn->errorMessage is set to
+ * a relevant message.
+ * If the query was sent, a new PGresult is returned (which could indicate
+ * either success or failure).  On success, the PGresult contains status
+ * PGRES_COMMAND_OK. The user is responsible for freeing the PGresult via
+ * PQclear() when done with it.
+ */
+PGresult *
+PQparameterSet(PGconn *conn, const char *parameter, const char *value)
+{
+	if (!PQexecStart(conn))
+		return NULL;
+	if (!PQsendParameterSet(conn, parameter, value))
+		return NULL;
+	return PQexecFinish(conn);
+}
+
+/*
+ * PQsendParameterSet
+ *	 Send a request for the server to change a run-time parameter setting.
+ *
+ * Returns 1 on success and 0 on failure.
+ */
+int
+PQsendParameterSet(PGconn *conn, const char *parameter, const char *value)
+{
+	PGcmdQueueEntry *entry = NULL;
+
+	if (!PQsendQueryStart(conn, true))
+		return 0;
+
+	entry = pqAllocCmdQueueEntry(conn);
+	if (entry == NULL)
+		return 0;				/* error msg already set */
+
+	/* construct the Close message */
+	if (pqPutMsgStart(PqMsg_ParameterSet, conn) < 0 ||
+		pqPuts(parameter, conn) < 0 ||
+		pqPuts(value, conn) < 0 ||
+		pqPutMsgEnd(conn) < 0)
+		goto sendFailed;
+
+	/* construct the Sync message */
+	if (conn->pipelineStatus == PQ_PIPELINE_OFF)
+	{
+		if (pqPutMsgStart(PqMsg_Sync, conn) < 0 ||
+			pqPutMsgEnd(conn) < 0)
+			goto sendFailed;
+	}
+
+	entry->queryclass = PGQUERY_PARAMETER_SET;
+
+	/*
+	 * Give the data a push (in pipeline mode, only if we're past the size
+	 * threshold).  In nonblock mode, don't complain if we're unable to send
+	 * it all; PQgetResult() will do any additional flushing needed.
+	 */
+	if (pqPipelineFlush(conn) < 0)
+		goto sendFailed;
+
+	/* OK, it's launched! */
+	pqAppendCmdQueueEntry(conn, entry);
+
+	return 1;
+
+sendFailed:
+	pqRecycleCmdQueueEntry(conn, entry);
+	/* error message should be set up already */
+	return 0;
+}
+
+
 /* ====== accessor funcs for PGresult ======== */
 
 ExecStatusType
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 89ce0d3962f..f5064ad3200 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -297,6 +297,28 @@ pqParseInput3(PGconn *conn)
 						conn->asyncStatus = PGASYNC_READY;
 					}
 					break;
+				case PqMsg_ParameterSetComplete:
+
+					/*
+					 * If we're doing PQsendParameterSet, we're done; else
+					 * ignore
+					 */
+					if (conn->cmd_queue_head &&
+						conn->cmd_queue_head->queryclass == PGQUERY_PARAMETER_SET)
+					{
+						if (!pgHavePendingResult(conn))
+						{
+							conn->result = PQmakeEmptyPGresult(conn,
+															   PGRES_COMMAND_OK);
+							if (!conn->result)
+							{
+								libpq_append_conn_error(conn, "out of memory");
+								pqSaveErrorResult(conn);
+							}
+						}
+						conn->asyncStatus = PGASYNC_READY;
+					}
+					break;
 				case PqMsg_ParameterStatus:
 					if (getParameterStatus(conn))
 						return;
@@ -2297,6 +2319,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 		}
 	}
 
+	if (conn->pq_protocol_managed_params && conn->pq_protocol_managed_params[0])
+		ADD_STARTUP_OPTION("_pq_.protocol_managed_params", conn->pq_protocol_managed_params);
+
 	/* Add trailing terminator */
 	if (packet)
 		packet[packet_len] = '\0';
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index b18e3deab6a..9ff0c0538c2 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -514,6 +514,23 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ParameterSet(F) or ParameterSetComplete(B) */
+static void
+pqTraceOutputU(FILE *f, bool toServer, const char *message, int *cursor)
+{
+	if (toServer)
+	{
+		fprintf(f, "ParameterSet\t");
+		pqTraceOutputString(f, message, cursor, false);
+		pqTraceOutputString(f, message, cursor, false);
+	}
+	else
+	{
+		fprintf(f, "ParameterSetComplete");
+		/* No message content */
+	}
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -589,6 +606,10 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			Assert(PqMsg_Close == PqMsg_CommandComplete);
 			pqTraceOutputC(conn->Pfdebug, toServer, message, &logCursor);
 			break;
+		case PqMsg_ParameterSet:
+			Assert(PqMsg_ParameterSet == PqMsg_ParameterSetComplete);
+			pqTraceOutputU(conn->Pfdebug, toServer, message, &logCursor);
+			break;
 		case PqMsg_CopyData:
 			/* Drop COPY data to reduce the overhead of logging. */
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 408ba495088..e675c0f7f98 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -555,6 +555,9 @@ extern PGresult *PQclosePortal(PGconn *conn, const char *portal);
 extern int	PQsendClosePrepared(PGconn *conn, const char *stmt);
 extern int	PQsendClosePortal(PGconn *conn, const char *portal);
 
+extern PGresult *PQparameterSet(PGconn *conn, const char *parameter, const char *value);
+extern int	PQsendParameterSet(PGconn *conn, const char *parameter, const char *value);
+
 /* Delete a PGresult */
 extern void PQclear(PGresult *res);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c379391a6b2..e89221836cd 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -322,7 +322,8 @@ typedef enum
 	PGQUERY_PREPARE,			/* Parse only (PQprepare) */
 	PGQUERY_DESCRIBE,			/* Describe Statement or Portal */
 	PGQUERY_SYNC,				/* Sync (at end of a pipeline) */
-	PGQUERY_CLOSE				/* Close Statement or Portal */
+	PGQUERY_CLOSE,				/* Close Statement or Portal */
+	PGQUERY_PARAMETER_SET		/* Set a server parameter */
 } PGQueryClass;
 
 /*
@@ -408,6 +409,7 @@ struct pg_conn
 	char	   *target_session_attrs;	/* desired session properties */
 	char	   *require_auth;	/* name of the expected auth method */
 	char	   *load_balance_hosts; /* load balance over hosts */
+	char	   *pq_protocol_managed_params; /* _pq_.protocol_managed_params */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 3c009ee1539..2efb6015666 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1046,6 +1046,257 @@ test_prepared(PGconn *conn)
 	fprintf(stderr, "ok\n");
 }
 
+static void
+test_parameter_set(PGconn *conn)
+{
+	PGresult   *res = NULL;
+	const char *val;
+
+	res = PQparameterSet(conn, "work_mem", "42MB");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to set non protocol parameters using ParameterSet");
+
+	/* Outside of a pipeline */
+	res = PQexec(conn, "SHOW _pq_.protocol_managed_params");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "") != 0)
+		pg_fatal("expected work_mem, got %s", val);
+
+	if (PQsendParameterSet(conn, "_pq_.protocol_managed_params", "work_mem") != 1)
+		pg_fatal("PQsendParameterSet failed: %s", PQerrorMessage(conn));
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+	PQclear(res);
+	res = PQgetResult(conn);
+	if (res != NULL)
+		pg_fatal("did not receive terminating NULL");
+
+	res = PQexec(conn, "SHOW _pq_.protocol_managed_params");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "work_mem") != 0)
+		pg_fatal("expected work_mem, got %s", val);
+
+	if (PQsendParameterSet(conn, "work_mem", "42MB") != 1)
+		pg_fatal("PQsendParameterSet failed: %s", PQerrorMessage(conn));
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQexec(conn, "SHOW work_mem");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "42MB") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+	PQclear(res);
+
+	res = PQexec(conn, "RESET ALL");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+
+	res = PQexec(conn, "SHOW _pq_.protocol_managed_params");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "work_mem") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+
+	res = PQexec(conn, "SHOW work_mem");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "42MB") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+	PQclear(res);
+
+	/* In a pipeline with other queries */
+	if (PQenterPipelineMode(conn) != 1)
+		pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn));
+	if (PQsendParameterSet(conn, "work_mem", "10MB") != 1)
+		pg_fatal("PQsendParameterSet failed: %s", PQerrorMessage(conn));
+	PQsendFlushRequest(conn);
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+	PQclear(res);
+	res = PQgetResult(conn);
+	if (res != NULL)
+		pg_fatal("did not receive terminating NULL");
+
+	/*
+	 * This is fine, but it opens an implicit transaction which should cause
+	 * the next ParameterSet message to fail
+	 */
+	if (PQsendQueryParams(conn, "SHOW work_mem", 0, NULL, NULL, NULL, NULL, 0) != 1)
+		pg_fatal("failed to send query: %s", PQerrorMessage(conn));
+	PQsendFlushRequest(conn);
+	res = PQgetResult(conn);
+	if (res == NULL)
+		pg_fatal("PQgetResult returned null when there's a pipeline item: %s",
+				 PQerrorMessage(conn));
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("unexpected result code %s from first pipeline item",
+				 PQresStatus(PQresultStatus(res)));
+
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "10MB") != 0)
+		pg_fatal("expected 10MB, got %s", val);
+	PQclear(res);
+
+	if (PQsendParameterSet(conn, "work_mem", "10MB") != 1)
+		pg_fatal("PQsendParameterSet failed: %s", PQerrorMessage(conn));
+	PQsendFlushRequest(conn);
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("PQsendParameterSet should not be allowed in a transaction");
+	if (PQpipelineSync(conn) != 1)
+		pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn));
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Sync should fail too");
+	res = PQgetResult(conn);
+	if (res != NULL)
+		pg_fatal("did not receive terminating NULL");
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_PIPELINE_SYNC)
+		pg_fatal("Unexpected result code %s instead of PGRES_PIPELINE_SYNC, error: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQexitPipelineMode(conn) != 1)
+		pg_fatal("exiting pipeline failed: %s", PQerrorMessage(conn));
+
+	/* In blocking mode */
+	res = PQparameterSet(conn, "work_mem", "42MB");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+	PQclear(res);
+	res = PQexec(conn, "SHOW work_mem");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQntuples(res) != 1)
+		pg_fatal("expected 1 result, got %d", PQntuples(res));
+
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "42MB") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+	PQclear(res);
+
+	/* In transaction */
+
+	res = PQexec(conn, "BEGIN");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to begin transaction: %s", PQerrorMessage(conn));
+
+	res = PQparameterSet(conn, "work_mem", "30MB");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("PQparameterSet should have failed in a transaction");
+
+	res = PQexec(conn, "SELECT 1");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("PQexec should have failed with 'current transaction is aborted'");
+
+	res = PQexec(conn, "ROLLBACK");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	res = PQexec(conn, "SHOW work_mem");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "42MB") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+	PQclear(res);
+
+	/* In a failed pipeline */
+	if (PQenterPipelineMode(conn) != 1)
+		pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn));
+	if (PQsendQueryParams(conn, "SELECT 0/0", 0, NULL, NULL, NULL, NULL, 0) != 1)
+		pg_fatal("failed to send query: %s", PQerrorMessage(conn));
+	if (PQsendParameterSet(conn, "work_mem", "12MB") != 1)
+		pg_fatal("PQsendParameterSet failed: %s", PQerrorMessage(conn));
+	if (PQpipelineSync(conn) != 1)
+		pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn));
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("PQexec should have failed with division by zero");
+	res = PQgetResult(conn);
+	if (res != NULL)
+		pg_fatal("did not receive terminating NULL");
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_PIPELINE_ABORTED)
+		pg_fatal("pipeline was not aborted");
+	res = PQgetResult(conn);
+	if (res != NULL)
+		pg_fatal("did not receive terminating NULL");
+	res = PQgetResult(conn);
+	if (PQresultStatus(res) != PGRES_PIPELINE_SYNC)
+		pg_fatal("Unexpected result code %s instead of PGRES_PIPELINE_SYNC, error: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	if (PQexitPipelineMode(conn) != 1)
+		pg_fatal("exiting pipeline failed: %s", PQerrorMessage(conn));
+
+	res = PQexec(conn, "SHOW work_mem");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("Expected tuples, got %s: %s",
+				 PQresStatus(PQresultStatus(res)), PQerrorMessage(conn));
+	val = PQgetvalue(res, 0, 0);
+	if (strcmp(val, "42MB") != 0)
+		pg_fatal("expected 42MB, got %s", val);
+	PQclear(res);
+
+	res = PQparameterSet(conn, "_pq_.protocol_managed_params", "role,session_authorization");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	res = PQparameterSet(conn, "work_mem", "42MB");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to set work_mem anymore using ParameterSet");
+
+	res = PQexec(conn, "SET SESSION AUTHORIZATION 'postgres'");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to set work_mem anymore using ParameterSet");
+
+	res = PQexec(conn, "SET ROLE 'postgres'");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to set work_mem anymore using ParameterSet");
+	res = PQparameterSet(conn, "_pq_.protocol_managed_params", "doesnotexist");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to use an unknown GUC");
+
+	res = PQparameterSet(conn, "_pq_.protocol_managed_params", "work_mem,_pq_.protocol_managed_params");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to use a protocol extension");
+
+	res = PQparameterSet(conn, "_pq_.protocol_managed_params", "\"");
+	if (PQresultStatus(res) != PGRES_FATAL_ERROR)
+		pg_fatal("Should not be allowed to use invalid list syntax");
+}
+
 /* Notice processor: print notices, and count how many we got */
 static void
 notice_processor(void *arg, const char *message)
@@ -1749,6 +2000,7 @@ print_test_list(void)
 	printf("disallowed_in_pipeline\n");
 	printf("multi_pipelines\n");
 	printf("nosync\n");
+	printf("parameter_set\n");
 	printf("pipeline_abort\n");
 	printf("pipeline_idle\n");
 	printf("pipelined_insert\n");
@@ -1853,6 +2105,8 @@ main(int argc, char **argv)
 		test_multi_pipelines(conn);
 	else if (strcmp(testname, "nosync") == 0)
 		test_nosync(conn);
+	else if (strcmp(testname, "parameter_set") == 0)
+		test_parameter_set(conn);
 	else if (strcmp(testname, "pipeline_abort") == 0)
 		test_pipeline_abort(conn);
 	else if (strcmp(testname, "pipeline_idle") == 0)
diff --git a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
index 71a11ddf259..173478548ed 100644
--- a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
+++ b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
@@ -37,7 +37,7 @@ for my $testname (@tests)
 {
 	my @extraargs = ('-r', $numrows);
 	my $cmptrace = grep(/^$testname$/,
-		qw(simple_pipeline nosync multi_pipelines prepared singlerow
+		qw(simple_pipeline nosync multi_pipelines parameter_set prepared singlerow
 		  pipeline_abort pipeline_idle transaction
 		  disallowed_in_pipeline)) > 0;
 
diff --git a/src/test/modules/libpq_pipeline/traces/parameter_set.trace b/src/test/modules/libpq_pipeline/traces/parameter_set.trace
new file mode 100644
index 00000000000..15c20f4f9c3
--- /dev/null
+++ b/src/test/modules/libpq_pipeline/traces/parameter_set.trace
@@ -0,0 +1,126 @@
+F	18	ParameterSet	 "work_mem" "42MB"
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42501" M "parameter cannot be set at the protocol level "work_mem"" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	38	Query	 "SHOW _pq_.protocol_managed_params"
+B	53	RowDescription	 1 "_pq_.protocol_managed_params" NNNN 0 NNNN 65535 -1 0
+B	10	DataRow	 1 0 ''
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	42	ParameterSet	 "_pq_.protocol_managed_params" "work_mem"
+F	4	Sync
+B	4	ParameterSetComplete
+B	5	ReadyForQuery	 I
+F	38	Query	 "SHOW _pq_.protocol_managed_params"
+B	53	RowDescription	 1 "_pq_.protocol_managed_params" NNNN 0 NNNN 65535 -1 0
+B	18	DataRow	 1 8 'work_mem'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	18	ParameterSet	 "work_mem" "42MB"
+F	4	Sync
+B	4	ParameterSetComplete
+B	5	ReadyForQuery	 I
+F	18	Query	 "SHOW work_mem"
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '42MB'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	14	Query	 "RESET ALL"
+B	10	CommandComplete	 "RESET"
+B	5	ReadyForQuery	 I
+F	38	Query	 "SHOW _pq_.protocol_managed_params"
+B	53	RowDescription	 1 "_pq_.protocol_managed_params" NNNN 0 NNNN 65535 -1 0
+B	18	DataRow	 1 8 'work_mem'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	18	Query	 "SHOW work_mem"
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '42MB'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	18	ParameterSet	 "work_mem" "10MB"
+F	4	Flush
+B	4	ParameterSetComplete
+F	21	Parse	 "" "SHOW work_mem" 0
+F	14	Bind	 "" "" 0 0 1 0
+F	6	Describe	 P ""
+F	9	Execute	 "" 0
+F	4	Flush
+B	4	ParseComplete
+B	4	BindComplete
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '10MB'
+B	9	CommandComplete	 "SHOW"
+F	18	ParameterSet	 "work_mem" "10MB"
+F	4	Flush
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "08P01" M "ParameterSet message is not allowed within a transaction" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	18	ParameterSet	 "work_mem" "42MB"
+F	4	Sync
+B	4	ParameterSetComplete
+B	5	ReadyForQuery	 I
+F	18	Query	 "SHOW work_mem"
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '42MB'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	10	Query	 "BEGIN"
+B	10	CommandComplete	 "BEGIN"
+B	5	ReadyForQuery	 T
+F	18	ParameterSet	 "work_mem" "30MB"
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "08P01" M "ParameterSet message is not allowed within a transaction" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 E
+F	13	Query	 "SELECT 1"
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "25P02" M "current transaction is aborted, commands ignored until end of transaction block" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 E
+F	13	Query	 "ROLLBACK"
+B	13	CommandComplete	 "ROLLBACK"
+B	5	ReadyForQuery	 I
+F	18	Query	 "SHOW work_mem"
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '42MB'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	18	Parse	 "" "SELECT 0/0" 0
+F	14	Bind	 "" "" 0 0 1 0
+F	6	Describe	 P ""
+F	9	Execute	 "" 0
+F	18	ParameterSet	 "work_mem" "12MB"
+F	4	Sync
+B	4	ParseComplete
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	18	Query	 "SHOW work_mem"
+B	33	RowDescription	 1 "work_mem" NNNN 0 NNNN 65535 -1 0
+B	14	DataRow	 1 4 '42MB'
+B	9	CommandComplete	 "SHOW"
+B	5	ReadyForQuery	 I
+F	60	ParameterSet	 "_pq_.protocol_managed_params" "role,session_authorization"
+F	4	Sync
+B	4	ParameterSetComplete
+B	5	ReadyForQuery	 I
+F	18	ParameterSet	 "work_mem" "42MB"
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42501" M "parameter cannot be set at the protocol level "work_mem"" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	41	Query	 "SET SESSION AUTHORIZATION 'postgres'"
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42501" M "parameter can only be set at the protocol level "session_authorization"" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	24	Query	 "SET ROLE 'postgres'"
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42501" M "parameter can only be set at the protocol level "role"" F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	46	ParameterSet	 "_pq_.protocol_managed_params" "doesnotexist"
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22023" M "invalid value for parameter "_pq_.protocol_managed_params": "doesnotexist"" D "Parameter "doesnotexist" is not recognized." F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	71	ParameterSet	 "_pq_.protocol_managed_params" "work_mem,_pq_.protocol_managed_params"
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22023" M "invalid value for parameter "_pq_.protocol_managed_params": "work_mem,_pq_.protocol_managed_params"" D "Parameter "_pq_.protocol_managed_params" is a protocol extension." F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	35	ParameterSet	 "_pq_.protocol_managed_params" """
+F	4	Sync
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22023" M "invalid value for parameter "_pq_.protocol_managed_params": """" D "List syntax is invalid." F "SSSS" L "SSSS" R "SSSS" \x00
+B	5	ReadyForQuery	 I
+F	4	Terminate
-- 
2.34.1

