From 037ffcf172165fcc823796e8fa99c0fc733a6408 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Fri, 5 Jan 2024 15:29:41 +0100 Subject: [PATCH v8 07/13] Add protocol message to change parameters This commit adds the ParamaterSet protocol message. This is simply the protocol equivalent of the SET command in SQL. Just like the Close protocol message is the equivalent of DEALLOCATE command in SQL. This new ParameterSet message thus provides a way of changing commands that avoids the need to escape the parameter to SET using SQL escaping rules. Escaping at the client side is generally frowned upon because it can easily be forgotten, which can lead to SQL injection security issues. In addition it allows intermediary components, such as connection poolers, to use this new message type to accept changes in configuration for that intermediary component itself. All of this without that component needing to parse SQL queries. Finally, it paves the way for a future commit which introduces a new GUC context that disallows changing a GUC with that context through SQL, but will still accept changes using the ParameterSet protocol message. --- doc/src/sgml/libpq.sgml | 48 +++++++++++++++ doc/src/sgml/protocol.sgml | 91 +++++++++++++++++++++++++++++ src/backend/postmaster/postmaster.c | 1 + src/backend/tcop/postgres.c | 24 ++++++++ src/include/libpq/protocol.h | 2 + src/interfaces/libpq/exports.txt | 4 +- src/interfaces/libpq/fe-exec.c | 76 ++++++++++++++++++++++++ src/interfaces/libpq/fe-protocol3.c | 22 +++++++ src/interfaces/libpq/fe-trace.c | 21 +++++++ src/interfaces/libpq/libpq-fe.h | 3 + src/interfaces/libpq/libpq-int.h | 3 +- 11 files changed, 293 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 248878da187..f40ff273b03 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -3436,6 +3436,33 @@ PGresult *PQclosePortal(PGconn *conn, const char *portalName); + + + PQparameterSetPQparameterSet + + + + Submits a request to change a protocol parameter, and waits for completion. + +PGresult *PQparameterSet(PGconn *conn, const char *parameter, const char *value); + + + + + allows a client to change + backend parameters on the current connection. This is identical to + sending a SET command, except that it is at the protocol level. + + + + parameter is the name of the parameter to + change, and value is the value to which to + change it. On success, a PGresult with status + PGRES_COMMAND_OK is returned. + + + + @@ -5129,6 +5156,27 @@ int PQsendClosePortal(PGconn *conn, const char *portalName); + + PQsendParameterSetPQsendParameterSet + + + + Submits a request to change a protocol parameter, without waiting for + completion. + +int PQsendParameterSet(PGconn *conn, const char *portalName); + + + This is an asynchronous version of : + it returns 1 if it was able to dispatch the request, and 0 if not. + After a successful call, call to + obtain the results. The function's parameters are handled + identically to . + + + + + PQgetResultPQgetResult diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 6c3e8a631d7..72f43dd51f1 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -1030,6 +1030,29 @@ SELCT 1/0; CloseComplete, or NoData messages. + + + Since protocol version 3.1, the ParameterSet message can be used to change + the value of a parameter. For most parameters this is equivalent to issuing + a SET command, but can be more convenient to parse for applications that + are inbetween a client and a server, such as a connection pooler. For + this message type is the only way + of changing their value after the initial StartupMessage. When changing + such protocol extension parameters the server behavior is slightly + different than for normal backend parameters. The server will not allow + changing protocol extension parameters while a transaction is active, and + when run in a pipeline each ParameterSet message will implicitly commit. + + + + + It is still possible to change protocol extension parameters with + ParameterSet in a pipeline together with other commands but only if no + transaction is active. This means that the ParameterSet messages either + need to be the first messages in the pipeline or any previous transaction + needs to have been explicitely committed. + + @@ -5271,6 +5294,74 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + ParameterSet (F) + + + + Byte1('U') + + + Identifies the message as a run-time parameter change. + + + + + + Int32 + + + Length of message contents in bytes, including self. + + + + + + String + + + The name of the run-time parameter to change. + + + + + + String + + + The new value of the parameter. + + + + + + + + + ParameterSetComplete (B) + + + + Byte1('U') + + + Identifies the message as a ParameterSet-complete indicator. + + + + + + Int32(4) + + + Length of message contents in bytes, including self. + + + + + + + Parse (F) diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index ad31e1046bb..0c14b830c30 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" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 1a34bd3715f..bcd4c84f3af 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -72,6 +72,7 @@ #include "tcop/tcopprot.h" #include "tcop/utility.h" #include "utils/guc_hooks.h" +#include "utils/guc_tables.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -427,6 +428,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 +4853,28 @@ 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); + + parameter_name = pq_getmsgstring(&input_message); + parameter_value = pq_getmsgstring(&input_message); + + start_xact_command(); + + SetConfigOption( + parameter_name, + parameter_value, + (superuser() ? PGC_SUSET : PGC_USERSET), + PGC_S_SESSION); + 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/include/libpq/protocol.h b/src/include/libpq/protocol.h index 4b8d4403656..c4040f99a66 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/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index b46c387f6f7..738b7ece78b 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -193,4 +193,6 @@ PQsendClosePrepared 190 PQsendClosePortal 191 PQchangePassword 192 PQsendPipelineSync 193 -PQunsupportedProtocolExtensions 194 +PQunsupportedProtocolExtensionParameters 194 +PQparameterSet 195 +PQsendParameterSet 196 diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index c02a9180b24..69e6b345b29 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -3365,6 +3365,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 249fa2984f3..129de550953 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; diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c9932fc8a6b..b69ecbd597d 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 d538f702b99..b5008e79d12 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -556,6 +556,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 7ad451c94f9..e54cc6e6c5f 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; /* -- 2.34.1