]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
libpq: Grease the protocol by default
authorJacob Champion <jchampion@postgresql.org>
Mon, 23 Feb 2026 18:48:20 +0000 (10:48 -0800)
committerJacob Champion <jchampion@postgresql.org>
Mon, 23 Feb 2026 18:48:20 +0000 (10:48 -0800)
Send PG_PROTOCOL_GREASE and _pq_.test_protocol_negotiation, which were
introduced in commit d8d7c5dc8, by default, and fail the connection if
the server attempts to claim support for them. The hope is to provide
feedback to noncompliant implementations and gain confidence in our
ability to advance the protocol. (See the other commit for details.)

To help end users navigate the situation, a link to our documentation
that explains the behavior is displayed. We append this to the error
message when the NegotiateProtocolVersion response is incorrect, or when
the peer sends an error during startup that appears to be grease-
related.

It's still possible for users to connect to servers that don't support
protocol negotiation, by adding max_protocol_version=3.0 to their
connection strings. Only the default connection behavior is impacted.

This commit is tracked as a PG19 open item and will be reverted before
RC1. (The implementation here doesn't handle negotiation with later
server versions, so it can't be released into the wild as a
five-year-supported feature. But an improved implementation might be
able to do so, in the future...)

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/DDPR5BPWH1RJ.1LWAK6QAURVAY%40jeltef.nl

doc/src/sgml/libpq.sgml
doc/src/sgml/protocol.sgml
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/fe-misc.c
src/interfaces/libpq/fe-protocol3.c
src/interfaces/libpq/libpq-int.h
src/test/modules/libpq_pipeline/libpq_pipeline.c

index 21e1ba34a4eb52544c51cc1748bf9ad2fece9db2..e08d46782cc837542fe46b4b780d061a5c2d1407 100644 (file)
@@ -2211,6 +2211,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
      <varlistentry id="libpq-connect-max-protocol-version" xreflabel="max_protocol_version">
       <term><literal>max_protocol_version</literal></term>
       <listitem>
+       <note>
+        <para>
+        During the PostgreSQL 19 beta period, libpq connections that do not
+        specify a <literal>max_protocol_version</literal> will "grease" the
+        handshake by sending unsupported startup parameters, including version
+        <literal>3.9999</literal>, in order to identify software that does not
+        correctly negotiate the connection. This replaces the default behavior
+        described below.
+        </para>
+        <para>
+        If you know that a server doesn't properly implement protocol version
+        negotiation, you can set <literal>max_protocol_version=3.0</literal> to
+        revert to the standard behavior (preferably after notifying the server's
+        maintainers that their software needs to be fixed).
+        </para>
+       </note>
+
        <para>
         Specifies the protocol version to request from the server.
         The default is to use version <literal>3.0</literal> of the
index 89ac680efd50b67e171d89dc6267fffd18536b6b..49f81676712b45b7fcd3be302d2a42bf78089f6f 100644 (file)
     by default.
    </para>
 
+   <note>
+     <para>
+      During the PostgreSQL 19 beta period, libpq will instead default to
+      requesting protocol version 3.9999, to test that servers and middleware
+      properly implement protocol version negotiation. Servers that support
+      negotiation will automatically downgrade to version 3.2 or 3.0. Users can
+      bypass this beta-only behavior by explicitly setting
+      <literal>max_protocol_version=3.0</literal> in their connection string.
+     </para>
+   </note>
+
    <para>
     A single server can support multiple protocol versions.  The initial
     startup-request message tells the server which protocol version the client
index a0d2f749811d33010f3de7274bae7dacb9004327..b42a0cb4c781fd448b6f2339411880bcdf830a36 100644 (file)
@@ -91,8 +91,9 @@ static int    ldapServiceLookup(const char *purl, PQconninfoOption *options,
 
 /* This is part of the protocol so just define it */
 #define ERRCODE_INVALID_PASSWORD "28P01"
-/* This too */
+/* These too */
 #define ERRCODE_CANNOT_CONNECT_NOW "57P03"
+#define ERRCODE_PROTOCOL_VIOLATION "08P01"
 
 /*
  * Cope with the various platform-specific ways to spell TCP keepalive socket
@@ -2142,15 +2143,13 @@ pqConnectOptions2(PGconn *conn)
        else
        {
                /*
-                * To not break connecting to older servers/poolers that do not yet
-                * support NegotiateProtocolVersion, default to the 3.0 protocol at
-                * least for a while longer. Except when min_protocol_version is set
-                * to something larger, then we might as well default to the latest.
+                * Default to PG_PROTOCOL_GREASE, which is larger than all real
+                * versions, to test negotiation. The server should automatically
+                * downgrade to a supported version.
+                *
+                * This behavior is for 19beta only. It will be reverted before RC1.
                 */
-               if (conn->min_pversion > PG_PROTOCOL(3, 0))
-                       conn->max_pversion = PG_PROTOCOL_LATEST;
-               else
-                       conn->max_pversion = PG_PROTOCOL(3, 0);
+               conn->max_pversion = PG_PROTOCOL_GREASE;
        }
 
        if (conn->min_pversion > conn->max_pversion)
@@ -4156,6 +4155,32 @@ keep_going:                                              /* We will come back to here until there is
                                        /* Check to see if we should mention pgpassfile */
                                        pgpassfileWarning(conn);
 
+                                       /*
+                                        * ...and whether we should mention grease. If the error
+                                        * message contains the PG_PROTOCOL_GREASE number (in
+                                        * major.minor, decimal, or hex format) or a complaint
+                                        * about a protocol violation before we've even started an
+                                        * authentication exchange, it's probably caused by a
+                                        * grease interaction.
+                                        */
+                                       if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+                                               !conn->auth_req_received)
+                                       {
+                                               const char *sqlstate = PQresultErrorField(conn->result,
+                                                                                                                                 PG_DIAG_SQLSTATE);
+
+                                               if ((sqlstate &&
+                                                        strcmp(sqlstate, ERRCODE_PROTOCOL_VIOLATION) == 0) ||
+                                                       (conn->errorMessage.len > 0 &&
+                                                        (strstr(conn->errorMessage.data, "3.9999") ||
+                                                         strstr(conn->errorMessage.data, "206607") ||
+                                                         strstr(conn->errorMessage.data, "3270F") ||
+                                                         strstr(conn->errorMessage.data, "3270f"))))
+                                               {
+                                                       libpq_append_grease_info(conn);
+                                               }
+                                       }
+
                                        CONNECTION_FAILED();
                                }
                                /* Handle NegotiateProtocolVersion */
@@ -4386,6 +4411,14 @@ keep_going:                                              /* We will come back to here until there is
                                        goto error_return;
                                }
 
+                               if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+                                       conn->pversion == PG_PROTOCOL_GREASE)
+                               {
+                                       libpq_append_conn_error(conn, "server incorrectly accepted \"grease\" protocol version 3.9999 without negotiation");
+                                       libpq_append_grease_info(conn);
+                                       goto error_return;
+                               }
+
                                /* Almost there now ... */
                                conn->status = CONNECTION_CHECK_TARGET;
                                goto keep_going;
index 5e54353fbfea7a9a656c6933fa9f62151fd1cf42..13775cfb8b90c6178356f5f01a0b6c7410deec87 100644 (file)
@@ -1423,3 +1423,21 @@ libpq_append_conn_error(PGconn *conn, const char *fmt,...)
 
        appendPQExpBufferChar(&conn->errorMessage, '\n');
 }
+
+/*
+ * For 19beta only, some protocol errors will have additional information
+ * appended to help with the "grease" campaign.
+ */
+void
+libpq_append_grease_info(PGconn *conn)
+{
+       /* translator: %s is a URL */
+       libpq_append_conn_error(conn,
+                                                       "\tThis indicates a bug in either the server being contacted\n"
+                                                       "\tor a proxy handling the connection. Please consider\n"
+                                                       "\treporting this to the maintainers of that software.\n"
+                                                       "\tFor more information, including instructions on how to\n"
+                                                       "\twork around this issue for now, visit\n"
+                                                       "\t\t%s",
+                                                       "https://www.postgresql.org/docs/devel/libpq-connect.html#LIBPQ-CONNECT-MAX-PROTOCOL-VERSION");
+}
index 90bbb2eba1f13b40362c8648884854042d226e14..8c1fda5caf02f3d7310062edff5e12a44c322f95 100644 (file)
@@ -1444,6 +1444,15 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
        int                     their_version;
        int                     num;
+       bool            found_test_protocol_negotiation;
+       bool            expect_test_protocol_negotiation;
+
+       /*
+        * During 19beta only, if protocol grease is in use, assume that it's the
+        * cause of any invalid messages encountered below. We'll print extra
+        * information for the end user in that case.
+        */
+       bool            need_grease_info = (conn->max_pversion == PG_PROTOCOL_GREASE);
 
        if (pqGetInt(&their_version, 4, conn) != 0)
                goto eof;
@@ -1504,6 +1513,7 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
                                                                PG_PROTOCOL_MAJOR(conn->min_pversion),
                                                                PG_PROTOCOL_MINOR(conn->min_pversion));
 
+               need_grease_info = false;       /* this is valid server behavior */
                goto failure;
        }
 
@@ -1511,9 +1521,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
        conn->pversion = their_version;
 
        /*
-        * We don't currently request any protocol extensions, so we don't expect
-        * the server to reply with any either.
+        * Check that all expected unsupported parameters are reported by the
+        * server.
         */
+       found_test_protocol_negotiation = false;
+       expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+
        for (int i = 0; i < num; i++)
        {
                if (pqGets(&conn->workBuffer, conn))
@@ -1525,7 +1538,29 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
                        libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
                        goto failure;
                }
-               libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+
+               /* Check if this is the expected test parameter */
+               if (expect_test_protocol_negotiation &&
+                       strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+               {
+                       found_test_protocol_negotiation = true;
+               }
+               else
+               {
+                       libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")",
+                                                                       conn->workBuffer.data);
+                       goto failure;
+               }
+       }
+
+       /*
+        * If we requested protocol grease, the server must report
+        * _pq_.test_protocol_negotiation as unsupported. This ensures
+        * comprehensive NegotiateProtocolVersion implementation.
+        */
+       if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+       {
+               libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
                goto failure;
        }
 
@@ -1534,6 +1569,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 eof:
        libpq_append_conn_error(conn, "received invalid protocol negotiation message: message too short");
 failure:
+       if (need_grease_info)
+               libpq_append_grease_info(conn);
        conn->asyncStatus = PGASYNC_READY;
        pqSaveErrorResult(conn);
        return 1;
@@ -2476,6 +2513,14 @@ build_startup_packet(const PGconn *conn, char *packet,
        if (conn->client_encoding_initial && conn->client_encoding_initial[0])
                ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+       /*
+        * Add the test_protocol_negotiation option when greasing, to test that
+        * servers properly report unsupported protocol options in addition to
+        * unsupported minor versions.
+        */
+       if (conn->pversion == PG_PROTOCOL_GREASE)
+               ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+
        /* Add any environment-driven GUC settings needed */
        for (next_eo = options; next_eo->envName; next_eo++)
        {
index fb6a7cbf15dec16512fbc25d41405b87f46fec30..bd7eb59f5f89d76d33630457454f6edc602c027e 100644 (file)
@@ -958,6 +958,7 @@ extern char *libpq_ngettext(const char *msgid, const char *msgid_plural, unsigne
 
 extern void libpq_append_error(PQExpBuffer errorMessage, const char *fmt,...) pg_attribute_printf(2, 3);
 extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3);
+extern void libpq_append_grease_info(PGconn *conn);
 
 /*
  * These macros are needed to let error-handling code be portable between
index ce1a9995f4656bb1cb248de9defa7ae7b06b095e..409b3a7fa455dba4f2d6bda7a38dc803e15a6aaf 100644 (file)
@@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn)
        Assert(max_protocol_version_index >= 0);
 
        /*
-        * Test default protocol_version
+        * Test default protocol_version (GREASE - should negotiate down to 3.2)
         */
        vals[max_protocol_version_index] = "";
        conn = PQconnectdbParams(keywords, vals, false);
@@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn)
                                 PQerrorMessage(conn));
 
        protocol_version = PQfullProtocolVersion(conn);
-       if (protocol_version != 30000)
-               pg_fatal("expected 30000, got %d", protocol_version);
+       if (protocol_version != 30002)
+               pg_fatal("expected 30002, got %d", protocol_version);
 
        PQfinish(conn);