From 7b0ad8c9ac1d7e10d097f869f35be87aaf2bae08 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 18 Jul 2025 10:06:13 -0700
Subject: [PATCH v2 1/2] libpq: Extend "read pending" check from SSL to GSS

An extra check for pending bytes in the SSL layer has been part of
pqReadReady() for a very long time (79ff2e96d). But when GSS transport
encryption was added, it didn't receive the same treatment. (As
79ff2e96d notes, "The bug that I fixed in this patch is exceptionally
hard to reproduce reliably.")

Without that check, it's possible to hit a hang in gssencmode, if the
server splits a large libpq message such that the final message in a
streamed response is part of the same wrapped token as the split
message:

    DataRowDataRowDataRowDataRowDataRowData
    -- token boundary --
    RowDataRowCommandCompleteReadyForQuery

If the split message takes up enough memory to nearly fill libpq's
receive buffer, libpq may return from pqReadData() before the later
messages are pulled out of the PqGSSRecvBuffer. Without additional
socket activity from the server, pqReadReady() (via pqSocketCheck())
will never again return true, hanging the connection.

Pull the pending-bytes check into the pqsecure API layer, where both SSL
and GSS now implement it.

Note that this does not fix the root problem! Third party clients of
libpq have no way to call pqsecure_read_is_pending() in their own
polling. This just brings the GSS implementation up to par with the
existing SSL workaround; a broader fix is left to a subsequent commit.
(However, pgtls_read_pending() is renamed to pgtls_read_is_pending(), to
avoid conflation with the forthcoming pgtls_drain_pending().)

Discussion: https://postgr.es/m/CAOYmi%2BmpymrgZ76Jre2dx_PwRniS9YZojwH0rZnTuiGHCsj0rA%40mail.gmail.com
---
 src/interfaces/libpq/fe-misc.c           |  6 ++----
 src/interfaces/libpq/fe-secure-gssapi.c  |  6 ++++++
 src/interfaces/libpq/fe-secure-openssl.c |  2 +-
 src/interfaces/libpq/fe-secure.c         | 19 +++++++++++++++++++
 src/interfaces/libpq/libpq-int.h         |  4 +++-
 5 files changed, 31 insertions(+), 6 deletions(-)

diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c
index dca44fdc5d2..434216ff89f 100644
--- a/src/interfaces/libpq/fe-misc.c
+++ b/src/interfaces/libpq/fe-misc.c
@@ -1099,14 +1099,12 @@ pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time)
 			return -1;
 		}
 
-#ifdef USE_SSL
-		/* Check for SSL library buffering read bytes */
-		if (forRead && conn->ssl_in_use && pgtls_read_pending(conn))
+		/* Check for SSL/GSS library buffering read bytes */
+		if (forRead && pqsecure_read_is_pending(conn))
 		{
 			/* short-circuit the select */
 			return 1;
 		}
-#endif
 	}
 
 	/* We will retry as long as we get EINTR */
diff --git a/src/interfaces/libpq/fe-secure-gssapi.c b/src/interfaces/libpq/fe-secure-gssapi.c
index bc9e1ce06fa..7c88e64cfd2 100644
--- a/src/interfaces/libpq/fe-secure-gssapi.c
+++ b/src/interfaces/libpq/fe-secure-gssapi.c
@@ -469,6 +469,12 @@ gss_read(PGconn *conn, void *recv_buffer, size_t length, ssize_t *ret)
 	return PGRES_POLLING_OK;
 }
 
+bool
+pg_GSS_read_is_pending(PGconn *conn)
+{
+	return PqGSSResultLength > PqGSSResultNext;
+}
+
 /*
  * Negotiate GSSAPI transport for a connection.  When complete, returns
  * PGRES_POLLING_OK.  Will return PGRES_POLLING_READING or
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 51dd7b9fec0..8f975561e51 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -231,7 +231,7 @@ rloop:
 }
 
 bool
-pgtls_read_pending(PGconn *conn)
+pgtls_read_is_pending(PGconn *conn)
 {
 	return SSL_pending(conn->ssl) > 0;
 }
diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c
index e686681ba15..94c97ec26fb 100644
--- a/src/interfaces/libpq/fe-secure.c
+++ b/src/interfaces/libpq/fe-secure.c
@@ -243,6 +243,25 @@ pqsecure_raw_read(PGconn *conn, void *ptr, size_t len)
 	return n;
 }
 
+/*
+ * Returns true if there are any bytes available in the transport buffer.
+ */
+bool
+pqsecure_read_is_pending(PGconn *conn)
+{
+#ifdef USE_SSL
+	if (conn->ssl_in_use)
+		return pgtls_read_is_pending(conn);
+#endif
+#ifdef ENABLE_GSS
+	if (conn->gssenc)
+		return pg_GSS_read_is_pending(conn);
+#endif
+
+	/* Plaintext connections have no transport buffer. */
+	return 0;
+}
+
 /*
  *	Write data to a secure connection.
  *
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a701c25038a..c38c1ea0086 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -810,6 +810,7 @@ extern int	pqWriteReady(PGconn *conn);
 extern PostgresPollingStatusType pqsecure_open_client(PGconn *);
 extern void pqsecure_close(PGconn *);
 extern ssize_t pqsecure_read(PGconn *, void *ptr, size_t len);
+extern bool pqsecure_read_is_pending(PGconn *);
 extern ssize_t pqsecure_write(PGconn *, const void *ptr, size_t len);
 extern ssize_t pqsecure_raw_read(PGconn *, void *ptr, size_t len);
 extern ssize_t pqsecure_raw_write(PGconn *, const void *ptr, size_t len);
@@ -848,7 +849,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
 /*
  *	Is there unread data waiting in the SSL read buffer?
  */
-extern bool pgtls_read_pending(PGconn *conn);
+extern bool pgtls_read_is_pending(PGconn *conn);
 
 /*
  *	Write data to a secure connection.
@@ -896,6 +897,7 @@ extern PostgresPollingStatusType pqsecure_open_gss(PGconn *conn);
  */
 extern ssize_t pg_GSS_write(PGconn *conn, const void *ptr, size_t len);
 extern ssize_t pg_GSS_read(PGconn *conn, void *ptr, size_t len);
+extern bool pg_GSS_read_is_pending(PGconn *conn);
 #endif
 
 /* === in fe-trace.c === */
-- 
2.34.1

