On Thu Jan 15, 2026 at 1:31 AM CET, Jacob Champion wrote:
Per offline confusion/discussion: I plan to work on a grease feature
for beta _regardless_ of whether a "production-grade"
max_protocol_version=grease option turns out to be viable before
feature freeze;
I saw that you committed a few patches. Given that you said you wanted
to wordsmith the last one, this seemed like a good time to send in some
of my own improvements in that regard. (I did not change the GREASE
mentions, since you clearly had something in mind for those).
0001 is mine an your squash commit, squashed together and rebased on top of
main.
0002 is the improvements to the docs. The one important thing is a
change from 3.2 to 3.0. Other than that it introduces a table for
tracking protocol extensions. That way other patches (like GoAway)
that introduce a protocol extension already have some location in
the docs where it can be listed.
0003-0005 are an attempt at making a bit more of a robust GREASE. 0003
makes it "harder to still implement negotation incorrectly". Then 0004
makes it not a hard failure if you connect with
max_protocol_version=grease to a new server. Then 0005 adds some
preliminary docs. These almost certainly need some more discussion. And
I don't expect them to necessarily get in for PG19, but if you like some
of it feel free to pick and take what you want from them. e.g. the
randomized protocol extensions are kinda nice IMO. But I'm not exactly
sure if the randomized version number is that much more useful though
than a fixed one.
From aeb772c789170e17c5c753445a8e0c06340dca81 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v5 1/5] libpq: Request protocol version 3.9999 to GREASE the
ecosystem
The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotiation when their users run into issues.
This patch defines 3.999 as an explicitly unsupported protocol version
number and _pq_.test_protocol_negotiation as an explicitly unsupported
protocol extension. It also starts requesting that version and protocol
extension by default. This change to the default will be reverted before
we release PG19 release candidates (when exactly to revert before that
time is TBD). The intent is to stress test the ecosystem for
servers/middleware that don't support protocol version negotiation, so
that those servers/middleware can implement the negotiation. This is
similar to the GREASE[1] mechanism that TLS has.
It's still possible for users to connect to servers that don't support
protocol negotiation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.
[1]: https://www.rfc-editor.org/rfc/rfc8701.html
Author: Jelte Fennema-Nio <[email protected]>
---
doc/src/sgml/libpq.sgml | 24 ++++++----
doc/src/sgml/protocol.sgml | 43 +++++++++++++++--
src/include/libpq/pqcomm.h | 9 ++++
src/interfaces/libpq/fe-connect.c | 20 ++++----
src/interfaces/libpq/fe-protocol3.c | 46 +++++++++++++++++--
.../modules/libpq_pipeline/libpq_pipeline.c | 6 +--
6 files changed, 121 insertions(+), 27 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21e1ba34a4e..fb6ce177f0d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,21 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<listitem>
<para>
Specifies the protocol version to request from the server.
- The default is to use version <literal>3.0</literal> of the
- <productname>PostgreSQL</productname> protocol, unless the connection
- string specifies a feature that relies on a higher protocol version,
- in which case the latest version supported by libpq is used. If the
- server does not support the protocol version requested by the client,
- the connection is automatically downgraded to a lower minor protocol
- version that the server supports. After the connection attempt has
- completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
- find out which exact protocol version was negotiated.
+ During the PostgreSQL 19 beta period, the default is to use
+ <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
+ Sustain Extensibility) value that tests proper protocol negotiation
+ implementation. If the server does not support the protocol version
+ requested by the client, the connection is automatically downgraded to
+ a lower minor protocol version that the server supports. After the
+ connection attempt has completed you can use
+ <xref linkend="libpq-PQfullProtocolVersion"/> to find out which exact
+ protocol version was negotiated.
+ </para>
+
+ <para>
+ For servers that don't properly implement protocol version negotiation,
+ you can set <literal>max_protocol_version=3.0</literal> to connect
+ successfully.
</para>
<para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index a2b528c481e..c61dd2bf948 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -192,10 +192,13 @@
<title>Protocol Versions</title>
<para>
- The current, latest version of the protocol is version 3.2. However, for
- backwards compatibility with old server versions and middleware that don't
- support the version negotiation yet, libpq still uses protocol version 3.0
- by default.
+ The current, latest version of the protocol is version 3.2. During the
+ PostgreSQL 19 beta period, libpq defaults 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. For servers that don't support
+ negotiation, users can connect by explicitly setting
+ <literal>max_protocol_version=3.0</literal> in their connection string.
</para>
<para>
@@ -238,6 +241,20 @@
</thead>
<tbody>
+ <row>
+ <entry>3.9999</entry>
+ <entry>-</entry>
+ <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
+ version. This version number is intentionally reserved and will never
+ be implemented. During the PostgreSQL 19 beta period, libpq requests
+ this version by default to test that servers and middleware properly
+ implement protocol version negotiation via
+ <literal>NegotiateProtocolVersion</literal>. Servers should respond
+ by downgrading to a supported version. This mechanism helps ensure
+ the ecosystem is ready for future protocol versions. libpq will revert
+ to defaulting to version 3.2 before the PostgreSQL 19 final release.
+ </entry>
+ </row>
<row>
<entry>3.2</entry>
<entry>PostgreSQL 18 and later</entry>
@@ -6147,6 +6164,24 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>_pq_.test_protocol_negotiation</literal></term>
+ <listitem>
+ <para>
+ A reserved protocol extension requested by libpq during the
+ PostgreSQL 19 beta period to test that servers properly implement
+ protocol version negotiation. When the client requests the GREASE
+ protocol version (3.9999), this parameter is automatically
+ included in the startup packet too. Servers should report it as
+ unsupported in their <literal>NegotiateProtocolVersion</literal>
+ response. In GREASE mode the connection will fail if the server
+ doesn't report this parameter as unsupported, ensuring
+ comprehensive implementation of protocol negotiation. This
+ parameter is reserved and will never actually be implemented by a
+ server.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
In addition to the above, other parameters may be listed.
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 1bbe5b9ee45..ca5127adfe5 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -104,6 +104,15 @@ is_unixsock_path(const char *path)
*/
#define PG_PROTOCOL_RESERVED_31 PG_PROTOCOL(3,1)
+/*
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for GREASE (Generate Random Extensions And Sustain Extensibility). This
+ * helps ensure that servers properly implement protocol version negotiation
+ * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
+ * the valid range but unlikely to ever be implemented.
+ */
+#define PG_PROTOCOL_GREASE PG_PROTOCOL(3,9999)
+
/*
* A client can send a cancel-current-operation request to the postmaster.
* This is uglier than sending it directly to the client's backend, but it
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a0d2f749811..9fcf094a36f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,15 +2142,12 @@ 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 the GREASE protocol version to test that servers
+ * properly implement NegotiateProtocolVersion. The server will
+ * automatically downgrade to a supported version. This will be
+ * changed to a supported version before the PG19 release.
*/
- 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)
@@ -4386,6 +4383,13 @@ 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 reserved GREASE protocol version 3.9999 without negotiation");
+ goto error_return;
+ }
+
/* Almost there now ... */
conn->status = CONNECTION_CHECK_TARGET;
goto keep_going;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 103428033ef..4f78b88b3a8 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1444,6 +1444,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
{
int their_version;
int num;
+ bool found_test_protocol_negotiation;
+ bool expect_test_protocol_negotiation;
if (pqGetInt(&their_version, 4, conn) != 0)
goto eof;
@@ -1471,6 +1473,13 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
goto failure;
}
+ /* The GREASE protocol version is intentionally unsupported and reserved */
+ if (their_version == PG_PROTOCOL_GREASE)
+ {
+ libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+ goto failure;
+ }
+
if (num < 0)
{
libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1499,9 +1508,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))
@@ -1513,7 +1525,27 @@ 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 the GREASE protocol version, 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;
}
@@ -2464,6 +2496,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 if we're using the GREASE
+ * protocol version. This tests that servers properly report unsupported
+ * protocol options in their NegotiateProtocolVersion response.
+ */
+ 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++)
{
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 0fb44be32ce..b819bcc273c 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -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);
base-commit: 9b9eaf08ab2dc22c691b22e59f1574e0f1bcc822
--
2.52.0
From 656ecd3b2b910d3e9f96e8a57323399a3e70a7bd Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Fri, 23 Jan 2026 23:13:00 +0100
Subject: [PATCH v5 2/5] fixup! libpq: Request protocol version 3.9999 to
GREASE the ecosystem
---
doc/src/sgml/protocol.sgml | 75 +++++++++++++++++++++++++++-----------
1 file changed, 54 insertions(+), 21 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c61dd2bf948..78d272ccc50 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -224,7 +224,7 @@
</para>
<para>
- <xref linkend="protocol-versions-table"/> shows the currently supported
+ <xref linkend="protocol-versions-table"/> shows the currently defined
protocol versions.
</para>
@@ -252,7 +252,7 @@
<literal>NegotiateProtocolVersion</literal>. Servers should respond
by downgrading to a supported version. This mechanism helps ensure
the ecosystem is ready for future protocol versions. libpq will revert
- to defaulting to version 3.2 before the PostgreSQL 19 final release.
+ to defaulting to version 3.0 before the PostgreSQL 19 final release.
</entry>
</row>
<row>
@@ -287,6 +287,56 @@
</tbody>
</tgroup>
</table>
+
+ </sect2>
+
+ <sect2 id="protocol-extensions">
+ <title>Protocol Extensions</title>
+
+ <para>
+ In addition to protocol version negotiation, the client can request
+ optional protocol extensions by including parameters in the startup message.
+ Protocol extension parameter names must be prefixed with
+ <literal>_pq_.</literal> to distinguish them from server parameters.
+ If the server does not recognize an extension, it will report it in the
+ NegotiateProtocolVersion message. <xref
+ linkend="protocol-extensions-table"/> shows the currently defined
+ protocol extensions.
+ </para>
+
+ <table id="protocol-extensions-table">
+ <title>Protocol Extensions</title>
+
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Extension</entry>
+ <entry>Supported Values</entry>
+ <entry>Introduced In</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>test_protocol_negotiation</literal></entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ <entry>
+ A reserved protocol extension requested by libpq to test that servers
+ properly implement protocol version negotiation. When the client
+ requests a GREASE protocol version, this parameter is automatically
+ included in the startup packet too. Servers should report it as
+ unsupported in their <literal>NegotiateProtocolVersion</literal>
+ response. In GREASE mode the connection will fail if the server
+ doesn't report this parameter as unsupported, ensuring comprehensive
+ implementation of protocol negotiation. This parameter is reserved and
+ will never actually be implemented by a server.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
</sect2>
</sect1>
@@ -6164,29 +6214,12 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
</para>
</listitem>
</varlistentry>
- <varlistentry>
- <term><literal>_pq_.test_protocol_negotiation</literal></term>
- <listitem>
- <para>
- A reserved protocol extension requested by libpq during the
- PostgreSQL 19 beta period to test that servers properly implement
- protocol version negotiation. When the client requests the GREASE
- protocol version (3.9999), this parameter is automatically
- included in the startup packet too. Servers should report it as
- unsupported in their <literal>NegotiateProtocolVersion</literal>
- response. In GREASE mode the connection will fail if the server
- doesn't report this parameter as unsupported, ensuring
- comprehensive implementation of protocol negotiation. This
- parameter is reserved and will never actually be implemented by a
- server.
- </para>
- </listitem>
- </varlistentry>
</variablelist>
In addition to the above, other parameters may be listed.
Parameter names beginning with <literal>_pq_.</literal> are
- reserved for use as protocol extensions, while others are
+ reserved for use as protocol extensions (see
+ <xref linkend="protocol-extensions"/>), while others are
treated as run-time parameters to be set at backend start
time. Such settings will be applied during backend start
(after parsing the command-line arguments if any) and will
--
2.52.0
From 3f074ffeaf9152c8a01350e2d5f69da0777c3f1c Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Thu, 15 Jan 2026 09:01:52 +0100
Subject: [PATCH v5 3/5] Implement more complicated grease
This actually implements the randomization that is part of the GREASE
acronym. When using max_protocol_version=grease, the version will now be
chosen from the range 3.9990-3.9999. The names of the reserved protocol
extensions are randomized too, as well as how many are requested
exactly.
---
src/include/libpq/pqcomm.h | 19 +++--
src/interfaces/libpq/fe-connect.c | 60 ++++++++++++--
src/interfaces/libpq/fe-protocol3.c | 121 +++++++++++++++++++++++-----
src/interfaces/libpq/libpq-int.h | 3 +
4 files changed, 167 insertions(+), 36 deletions(-)
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index ca5127adfe5..d38789bc35b 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -105,13 +105,20 @@ is_unixsock_path(const char *path)
#define PG_PROTOCOL_RESERVED_31 PG_PROTOCOL(3,1)
/*
- * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
- * for GREASE (Generate Random Extensions And Sustain Extensibility). This
- * helps ensure that servers properly implement protocol version negotiation
- * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
- * the valid range but unlikely to ever be implemented.
+ * GREASE (Generate Random Extensions And Sustain Extensibility) version range.
+ * These are intentionally unsupported protocol versions used to ensure servers
+ * properly implement protocol version negotiation via NegotiateProtocolVersion.
+ * The range 3.9990-3.9999 was chosen to be safely within the valid range but
+ * unlikely to ever be implemented. A random version from this range is selected
+ * at connection time to prevent implementations from accidentally depending on
+ * specific GREASE values.
*/
-#define PG_PROTOCOL_GREASE PG_PROTOCOL(3,9999)
+#define PG_PROTOCOL_GREASE_MIN PG_PROTOCOL(3,9990)
+#define PG_PROTOCOL_GREASE_MAX PG_PROTOCOL(3,9999)
+
+/* Check if a protocol version is in the GREASE range */
+#define PG_PROTOCOL_IS_GREASE(v) \
+ ((v) >= PG_PROTOCOL_GREASE_MIN && (v) <= PG_PROTOCOL_GREASE_MAX)
/*
* A client can send a cancel-current-operation request to the postmaster.
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9fcf094a36f..fdc05cd3243 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,12 +2142,45 @@ pqConnectOptions2(PGconn *conn)
else
{
/*
- * Default to the GREASE protocol version to test that servers
- * properly implement NegotiateProtocolVersion. The server will
- * automatically downgrade to a supported version. This will be
- * changed to a supported version before the PG19 release.
+ * Default to a GREASE protocol version to test that servers properly
+ * implement NegotiateProtocolVersion. The server will automatically
+ * downgrade to a supported version. This will be changed to a
+ * supported version before the PG19 release.
*/
- conn->max_pversion = PG_PROTOCOL_GREASE;
+ conn->max_pversion = PG_PROTOCOL_GREASE_MAX;
+ }
+
+ /*
+ * If using GREASE, randomize the version, the number of GREASE parameters
+ * (0-5), and each parameter's ID. This prevents implementations from
+ * accidentally depending on specific GREASE values.
+ */
+ if (PG_PROTOCOL_IS_GREASE(conn->max_pversion))
+ {
+ int grease_range;
+
+ libpq_prng_init(conn);
+ grease_range = PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MAX) -
+ PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MIN);
+ conn->max_pversion = PG_PROTOCOL(3,
+ PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MIN) +
+ pg_prng_uint64_range(&conn->prng_state, 0, grease_range));
+ conn->ngrease_params = pg_prng_uint64_range(&conn->prng_state, 0, 5);
+
+ /* Initialize prefix indices and shuffle them (Fisher-Yates) */
+ for (int g = 0; g < 5; g++)
+ conn->grease_prefix[g] = g;
+ for (int g = 4; g > 0; g--)
+ {
+ int j = pg_prng_uint64_range(&conn->prng_state, 0, g);
+ int tmp = conn->grease_prefix[g];
+
+ conn->grease_prefix[g] = conn->grease_prefix[j];
+ conn->grease_prefix[j] = tmp;
+ }
+
+ for (int g = 0; g < conn->ngrease_params; g++)
+ conn->grease_params[g] = (uint16) pg_prng_uint64_range(&conn->prng_state, 0, 0xFFFF);
}
if (conn->min_pversion > conn->max_pversion)
@@ -4383,10 +4416,11 @@ 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)
+ if (PG_PROTOCOL_IS_GREASE(conn->max_pversion) &&
+ PG_PROTOCOL_IS_GREASE(conn->pversion))
{
- libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+ libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.%d without negotiation",
+ PG_PROTOCOL_MINOR(conn->pversion));
goto error_return;
}
@@ -8341,6 +8375,16 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
return true;
}
+ if (strcmp(value, "grease") == 0)
+ {
+ /*
+ * Use a placeholder; the actual random version is selected later when
+ * the PRNG is initialized.
+ */
+ *result = PG_PROTOCOL_GREASE_MAX;
+ return true;
+ }
+
libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
context, value);
return false;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 4f78b88b3a8..c0e0142798a 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -31,6 +31,20 @@
#include "mb/pg_wchar.h"
#include "port/pg_bswap.h"
+/*
+ * GREASE parameter name prefixes. Each GREASE parameter uses a different
+ * prefix to prevent implementations from pattern-matching on a single prefix.
+ * The full parameter name is the prefix followed by a random 4-digit hex
+ * number, e.g. "_pq_.protocol_grease_a1b2".
+ */
+static const char *const grease_prefixes[5] = {
+ "_pq_.test_protocol_negotiation_",
+ "_pq_.negotiation_test_",
+ "_pq_.protocol_grease_",
+ "_pq_.grease_the_server_",
+ "_pq_.always_unknown_extension_"
+};
+
/*
* This macro lists the backend message types that could be "long" (more
* than a couple of kilobytes).
@@ -1444,8 +1458,9 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
{
int their_version;
int num;
- bool found_test_protocol_negotiation;
- bool expect_test_protocol_negotiation;
+ int ngrease_found;
+ bool grease_found[5] = {false};
+ bool expect_grease;
if (pqGetInt(&their_version, 4, conn) != 0)
goto eof;
@@ -1473,10 +1488,24 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
goto failure;
}
- /* The GREASE protocol version is intentionally unsupported and reserved */
- if (their_version == PG_PROTOCOL_GREASE)
+ /* GREASE protocol versions are intentionally unsupported and reserved */
+ if (PG_PROTOCOL_IS_GREASE(their_version))
+ {
+ libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.%d",
+ PG_PROTOCOL_MINOR(their_version));
+ goto failure;
+ }
+
+ /*
+ * If the server responds with a version newer than what this libpq
+ * supports, disconnect. This can happen if we sent a GREASE version and a
+ * future server legitimately supports a newer minor version than us.
+ */
+ if (their_version > PG_PROTOCOL_LATEST)
{
- libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+ libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
+ PG_PROTOCOL_MINOR(their_version),
+ PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
goto failure;
}
@@ -1509,13 +1538,16 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
/*
* Check that all expected unsupported parameters are reported by the
- * server.
+ * server. In GREASE mode, we expect all our GREASE parameters to be
+ * reported as unsupported.
*/
- found_test_protocol_negotiation = false;
- expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+ ngrease_found = 0;
+ expect_grease = PG_PROTOCOL_IS_GREASE(conn->max_pversion);
for (int i = 0; i < num; i++)
{
+ bool matched = false;
+
if (pqGets(&conn->workBuffer, conn))
{
goto eof;
@@ -1526,12 +1558,31 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
goto failure;
}
- /* Check if this is the expected test parameter */
- if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+ /* Check if this matches any of our expected GREASE parameters */
+ if (expect_grease)
{
- found_test_protocol_negotiation = true;
+ for (int j = 0; j < conn->ngrease_params; j++)
+ {
+ char expected_opt[32];
+
+ snprintf(expected_opt, sizeof(expected_opt), "%s%04x",
+ grease_prefixes[conn->grease_prefix[j]], conn->grease_params[j]);
+ if (strcmp(conn->workBuffer.data, expected_opt) == 0)
+ {
+ if (grease_found[j])
+ {
+ libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported duplicate unsupported parameter (\"%s\")", conn->workBuffer.data);
+ goto failure;
+ }
+ grease_found[j] = true;
+ ngrease_found++;
+ matched = true;
+ break;
+ }
+ }
}
- else
+
+ if (!matched)
{
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;
@@ -1539,13 +1590,28 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
}
/*
- * If we requested the GREASE protocol version, the server must report
- * _pq_.test_protocol_negotiation as unsupported. This ensures
- * comprehensive NegotiateProtocolVersion implementation.
+ * If we requested a GREASE protocol version, the server must report all
+ * our GREASE options as unsupported. This ensures comprehensive
+ * NegotiateProtocolVersion implementation.
*/
- if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+ if (expect_grease && ngrease_found != conn->ngrease_params)
{
- libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
+ PQExpBufferData missing;
+
+ initPQExpBuffer(&missing);
+ for (int j = 0; j < conn->ngrease_params; j++)
+ {
+ if (!grease_found[j])
+ {
+ if (missing.len > 0)
+ appendPQExpBufferStr(&missing, ", ");
+ appendPQExpBuffer(&missing, "%s%04x",
+ grease_prefixes[conn->grease_prefix[j]], conn->grease_params[j]);
+ }
+ }
+ libpq_append_conn_error(conn, "server did not report unsupported GREASE parameter(s): %s",
+ missing.data);
+ termPQExpBuffer(&missing);
goto failure;
}
@@ -2497,12 +2563,23 @@ build_startup_packet(const PGconn *conn, char *packet,
ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
/*
- * Add the test protocol negotiation option if we're using the GREASE
- * protocol version. This tests that servers properly report unsupported
- * protocol options in their NegotiateProtocolVersion response.
+ * Add GREASE protocol options if we're using a GREASE protocol version.
+ * This tests that servers properly report unsupported protocol options in
+ * their NegotiateProtocolVersion response. Each option name includes a
+ * random suffix to prevent implementations from depending on specific
+ * GREASE values. The number of options (0-5) is also randomized.
*/
- if (conn->pversion == PG_PROTOCOL_GREASE)
- ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+ if (PG_PROTOCOL_IS_GREASE(conn->pversion))
+ {
+ for (int i = 0; i < conn->ngrease_params; i++)
+ {
+ char grease_opt[32];
+
+ snprintf(grease_opt, sizeof(grease_opt), "%s%04x",
+ grease_prefixes[conn->grease_prefix[i]], conn->grease_params[i]);
+ ADD_STARTUP_OPTION(grease_opt, "");
+ }
+ }
/* Add any environment-driven GUC settings needed */
for (next_eo = options; next_eo->envName; next_eo++)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index fb6a7cbf15d..a599d57e325 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -547,6 +547,9 @@ struct pg_conn
uint8 *scram_server_key_binary; /* binary SCRAM server key */
ProtocolVersion min_pversion; /* protocol version to request */
ProtocolVersion max_pversion; /* protocol version to request */
+ int ngrease_params; /* number of GREASE parameters (0-5) */
+ int grease_prefix[5]; /* which prefix to use for each param */
+ uint16 grease_params[5]; /* random IDs for GREASE parameter names */
/* Miscellaneous stuff */
int be_pid; /* PID of backend --- needed for cancels */
--
2.52.0
From 60ed7f5624b007b7ab7b27167fa44f0aa51692ab Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Thu, 15 Jan 2026 09:58:07 +0100
Subject: [PATCH v5 4/5] libpq: Reconnect if requested grease but version is
newer
While max_protocol_version=grease is mostly meant for testing, people
might still use it in production environments. So it seems better to
support reconnecting automatically if the server responds with a
downgrade to a higher version than libpq supports.
---
src/interfaces/libpq/fe-connect.c | 23 ++++++++++++++++---
src/interfaces/libpq/fe-protocol3.c | 34 +++++++++++++++++++----------
2 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index fdc05cd3243..738655488f3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4196,10 +4196,27 @@ keep_going: /* We will come back to here until there is
libpq_append_conn_error(conn, "received duplicate protocol negotiation message");
goto error_return;
}
- if (pqGetNegotiateProtocolVersion3(conn))
{
- /* pqGetNegotiateProtocolVersion3 set error already */
- goto error_return;
+ int rc = pqGetNegotiateProtocolVersion3(conn);
+
+ if (rc == 1)
+ {
+ /*
+ * pqGetNegotiateProtocolVersion3 set error
+ * already
+ */
+ goto error_return;
+ }
+ if (rc == 2)
+ {
+ /*
+ * Server proposed newer version than libpq
+ * supports; retry with the latest supported
+ * version.
+ */
+ need_new_connection = true;
+ goto keep_going;
+ }
}
conn->pversion_negotiated = true;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index c0e0142798a..b7e30516516 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1452,6 +1452,8 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
* Entry: 'v' message type and length have already been consumed.
* Exit: returns 0 if successfully consumed message.
* returns 1 on failure. The error message is filled in.
+ * returns 2 if server proposed a newer version and we were using GREASE;
+ * caller should retry the connection with the latest supported version.
*/
int
pqGetNegotiateProtocolVersion3(PGconn *conn)
@@ -1465,6 +1467,21 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
if (pqGetInt(&their_version, 4, conn) != 0)
goto eof;
+ /*
+ * If the server responds with a version newer than what this libpq
+ * supports, and we were using GREASE, retry with the latest supported
+ * version. This can happen if a future server legitimately supports a
+ * newer minor version than us. We don't try to parse the rest of the
+ * message since we don't know what format a newer protocol version might
+ * use. The GREASE checks for protocol extensions will still happen on the
+ * retry.
+ */
+ if (their_version > PG_PROTOCOL_LATEST && PG_PROTOCOL_IS_GREASE(conn->pversion))
+ {
+ conn->max_pversion = PG_PROTOCOL_LATEST;
+ return 2;
+ }
+
if (pqGetInt(&num, 4, conn) != 0)
goto eof;
@@ -1496,22 +1513,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
goto failure;
}
- /*
- * If the server responds with a version newer than what this libpq
- * supports, disconnect. This can happen if we sent a GREASE version and a
- * future server legitimately supports a newer minor version than us.
- */
- if (their_version > PG_PROTOCOL_LATEST)
+ if (num < 0)
{
- libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
- PG_PROTOCOL_MINOR(their_version),
- PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
+ libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
goto failure;
}
- if (num < 0)
+ if (their_version > PG_PROTOCOL_LATEST)
{
- libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
+ libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
+ PG_PROTOCOL_MINOR(their_version),
+ PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
goto failure;
}
--
2.52.0
From ec791d56482888a5a52171c73c42aa32ac300d0d Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Fri, 23 Jan 2026 22:32:20 +0100
Subject: [PATCH v5 5/5] Update libpq docs for better grease
Kept separate from the previous two patches for now while we discuss the
actual details of the implementation.
---
doc/src/sgml/libpq.sgml | 26 ++++++++++-----
doc/src/sgml/protocol.sgml | 68 ++++++++++++++++++++++++--------------
2 files changed, 62 insertions(+), 32 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index fb6ce177f0d..d785f18914f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,10 +2213,17 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<listitem>
<para>
Specifies the protocol version to request from the server.
- During the PostgreSQL 19 beta period, the default is to use
- <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
- Sustain Extensibility) value that tests proper protocol negotiation
- implementation. If the server does not support the protocol version
+ During the PostgreSQL 19 beta period, the default is
+ <literal>grease</literal>, which requests a random protocol version in
+ the range 3.9990-3.9999 and includes 0-5 random GREASE startup
+ parameters. This tests that servers and middleware properly implement
+ protocol version negotiation. The default will be changed to
+ <literal>latest</literal> before PostgreSQL 19 is released, but the
+ <literal>grease</literal> option will remain available for testing.
+ </para>
+
+ <para>
+ If the server does not support the protocol version
requested by the client, the connection is automatically downgraded to
a lower minor protocol version that the server supports. After the
connection attempt has completed you can use
@@ -2232,10 +2239,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<para>
The current supported values are
- <literal>3.0</literal>, <literal>3.2</literal>,
- and <literal>latest</literal>. The <literal>latest</literal> value is
- equivalent to the latest protocol version supported by the libpq
- version being used, which is currently <literal>3.2</literal>.
+ <literal>3.0</literal>,
+ <literal>3.2</literal>,
+ <literal>latest</literal>, and
+ <literal>grease</literal>.
+ The <literal>latest</literal> value is equivalent to the latest
+ protocol version supported by the libpq version being used, which is
+ currently <literal>3.2</literal>.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 78d272ccc50..922b7ca3b1d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -193,12 +193,13 @@
<para>
The current, latest version of the protocol is version 3.2. During the
- PostgreSQL 19 beta period, libpq defaults 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. For servers that don't support
- negotiation, users can connect by explicitly setting
- <literal>max_protocol_version=3.0</literal> in their connection string.
+ PostgreSQL 19 beta period, libpq defaults to requesting a random protocol
+ version in the range 3.9990-3.9999 (the GREASE range) 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. For servers that don't support negotiation, users can connect by
+ explicitly setting <literal>max_protocol_version=3.0</literal> in their
+ connection string.
</para>
<para>
@@ -242,17 +243,17 @@
<tbody>
<row>
- <entry>3.9999</entry>
+ <entry>3.9990-3.9999</entry>
<entry>-</entry>
<entry>GREASE (Generate Random Extensions And Sustain Extensibility)
- version. This version number is intentionally reserved and will never
- be implemented. During the PostgreSQL 19 beta period, libpq requests
- this version by default to test that servers and middleware properly
- implement protocol version negotiation via
+ version range. These version numbers are intentionally reserved and
+ will never be implemented. Clients can request a random version from
+ this range to test that servers and middleware properly implement
+ protocol version negotiation via
<literal>NegotiateProtocolVersion</literal>. Servers should respond
by downgrading to a supported version. This mechanism helps ensure
- the ecosystem is ready for future protocol versions. libpq will revert
- to defaulting to version 3.0 before the PostgreSQL 19 final release.
+ the ecosystem is ready for future protocol versions. libpq supports
+ this via <literal>max_protocol_version=grease</literal>.
</entry>
</row>
<row>
@@ -319,21 +320,40 @@
<tbody>
<row>
- <entry><literal>test_protocol_negotiation</literal></entry>
+ <entry><literal>test_protocol_negotiation_XXXX</literal></entry>
<entry>-</entry>
<entry>-</entry>
- <entry>
- A reserved protocol extension requested by libpq to test that servers
- properly implement protocol version negotiation. When the client
- requests a GREASE protocol version, this parameter is automatically
- included in the startup packet too. Servers should report it as
- unsupported in their <literal>NegotiateProtocolVersion</literal>
- response. In GREASE mode the connection will fail if the server
- doesn't report this parameter as unsupported, ensuring comprehensive
- implementation of protocol negotiation. This parameter is reserved and
- will never actually be implemented by a server.
+ <entry morerows="4">
+ Reserved GREASE (Generate Random Extensions And Sustain Extensibility)
+ protocol extensions. The XXXX can be replaced for any random hex
+ number. These protocol extensions will never be implemented by a
+ server, and they should thus report them as unsupported in their
+ <literal>NegotiateProtocolVersion</literal> response. Clients are free
+ to request them to ensure a comprehensive implementation of the
+ protocol negotiation. As an example libpq sends 0-5 of these parameters
+ when <literal>max_protocol_version=grease</literal>.
</entry>
</row>
+ <row>
+ <entry><literal>negotiation_test_XXXX</literal></entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ </row>
+ <row>
+ <entry><literal>protocol_grease_XXXX</literal></entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ </row>
+ <row>
+ <entry><literal>grease_the_server_XXXX</literal></entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ </row>
+ <row>
+ <entry><literal>always_unknown_extension_XXXX</literal></entry>
+ <entry>-</entry>
+ <entry>-</entry>
+ </row>
</tbody>
</tgroup>
</table>
--
2.52.0