From 3f074ffeaf9152c8a01350e2d5f69da0777c3f1c Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
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

