Hello,
The functions in src/interfaces/libpq/fe-trace.c that print a traced
protocol message field by field trust the counts and lengths found inside
the message: the length of a DataRow field, the number of fields in a
RowDescription, the zero byte that ends a string, and so on. None of them
is checked against the message's length word, so a malformed message from
the server makes a client that has called PQtrace() read past the end of
that message in its input buffer. Messages that libpq discards without
parsing them (a DataRow with no RowDescription before it, a message of a
type it doesn't expect) are read by the tracer alone, so the checks in
fe-protocol3.c don't cover them. The tracer also takes only -1 as a NULL
DataRow field, while getAnotherTuple() takes any negative length as NULL.
I sent this to security@ first. Alvaro answered that it isn't a
vulnerability, since it needs a hostile server or network, and that it
should go through the normal channels, possibly on master only, so here
it is.
The attached series applies to master (3c5a4a867f2):
0001 gives every reader in fe-trace.c (Byte1, Int16, Int32, String,
Nchar) the end of the message, and they stop there, so no
per-message printer can read past a message whatever it claims. A
message whose contents don't fit is printed as far as it goes and
reported as "mismatched message length: contents exceed length N".
Strings are measured within the message instead of with strlen().
A DataRow field with any negative length is printed as NULL, as
libpq reads the row, and CopyBothResponse reads its count of column
formats as CopyInResponse and CopyOutResponse do. Well-formed
messages are traced as before.
0002 stops tracing a pre-3.0 error message as a protocol 3 message. Such
a message has no length word, and since b8b3f861fbd every message
received during connection startup goes to the tracer, which then
took its first four characters for a length.
0003 adds a test. libpq_trace_regress includes fe-trace.c, as
test-oauth-curl.c includes oauth-curl.c, and traces one message
given on its command line in a buffer of exactly the message's size,
so that AddressSanitizer catches any read past its end;
t/007_trace.pl runs it over well-formed and malformed messages.
On master built with -Db_sanitize=address, 007_trace.pl fails 33 checks,
with heap-buffer-overflow reports in pqTraceOutputNchar,
pqTraceOutputInt32, pqTraceOutputInt16 and in strlen() called from
pqTraceOutputString. With the series all 84 checks pass, and the libpq
and libpq_pipeline suites pass with their traces unchanged.
0001's readers are as old as fe-trace.c (v14); 0002 concerns code that
came with b8b3f861fbd in v18.
This overlaps textually with "Add PQTRACE env to enable protocol tracing
in libpq" (commitfest 7081), which also reworks fe-trace.c. Its
environment variable would let tracing be switched on for any libpq
program, which makes these reads easier to reach. Whichever of the two
lands first, I'll rebase this series onto it as needed.
As an aside, the salt of an AuthenticationMD5Password request is never
traced, so every trace of an MD5 login reports "mismatched message
length: consumed 8, expected 12" for that message. That's a separate
small fix, which I can send in its own thread.
I'll add this to the next commitfest.
Thanks,
Ana Bobillo
From 515d8221cbab84e57d38e78fd6c5f0e4f9da9966 Mon Sep 17 00:00:00 2001
From: Ana Bobillo <[email protected]>
Date: Thu, 22 Oct 2026 20:56:23 +0200
Subject: [PATCH v1 1/3] libpq: Don't read past the end of a message when
tracing it
From: Ana Bobillo <[email protected]>
The functions that print a traced protocol message field by field
trusted the counts and lengths inside the message: the length of a
DataRow field, the number of fields in a RowDescription, the zero byte
that ends a string, and so on. For messages received from the server
those come from the other side of the connection, so a malformed
message made PQtrace() read past the end of the message, into the rest
of the input buffer and beyond it. Messages that libpq discards
without parsing them, such as a DataRow without a RowDescription or a
message of a type it doesn't expect, are read by the tracer alone.
Give each of these functions the end of the message, and have them stop
there. A message whose contents don't fit in its length is printed as
far as it goes and reported as "mismatched message length: contents
exceed length N". Strings are now measured within the message, instead
of with strlen() or from fprintf()'s return value. Also treat any
negative DataRow field length as NULL, as libpq does when it reads the
row, and have CopyBothResponse read its count of column formats like
CopyInResponse and CopyOutResponse do.
Well-formed messages are traced as before.
---
src/interfaces/libpq/fe-trace.c | 447 +++++++++++++++++++-------------
1 file changed, 262 insertions(+), 185 deletions(-)
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 2901fa5b451..764ae86691d 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -100,34 +100,70 @@ pqTraceFormatTimestamp(char *timestr, size_t ts_len)
".%06u", (unsigned int) (tval.tv_usec));
}
+/*
+ * The functions below print one field of a message and advance the cursor
+ * past it. 'end' is the cursor position just past the end of the message.
+ * The counts and lengths inside a message come from the other side of the
+ * connection and can't be trusted, so they never read at or past 'end': if
+ * the message is too short for a field, they print as much of it as there is
+ * and move the cursor to end + 1. That marks the message as truncated, so
+ * that they read nothing more from it, and the caller can report it.
+ */
+
+/*
+ * pqTraceCheckLength: check that the next n bytes are within the message
+ *
+ * If they aren't, mark the message as truncated and return false.
+ */
+static bool
+pqTraceCheckLength(int n, int *cursor, int end)
+{
+ if (*cursor <= end && n >= 0 && n <= end - *cursor)
+ return true;
+ *cursor = end + 1;
+ return false;
+}
+
/*
* pqTraceOutputByte1: output a 1-char message to the log
+ *
+ * Returns the byte, or \0 if the message is truncated.
*/
-static void
-pqTraceOutputByte1(FILE *pfdebug, const char *data, int *cursor)
+static char
+pqTraceOutputByte1(FILE *pfdebug, const char *data, int *cursor, int end)
{
- const char *v = data + *cursor;
+ char v;
+
+ if (!pqTraceCheckLength(1, cursor, end))
+ return '\0';
+ v = data[*cursor];
/*
* Show non-printable data in hex format, including the terminating \0
* that completes ErrorResponse and NoticeResponse messages.
*/
- if (!isprint((unsigned char) *v))
- fprintf(pfdebug, " \\x%02x", (unsigned char) *v);
+ if (!isprint((unsigned char) v))
+ fprintf(pfdebug, " \\x%02x", (unsigned char) v);
else
- fprintf(pfdebug, " %c", *v);
+ fprintf(pfdebug, " %c", v);
*cursor += 1;
+
+ return v;
}
/*
* pqTraceOutputInt16: output a 2-byte integer message to the log
+ *
+ * Returns the integer, or 0 if the message is truncated.
*/
static int
-pqTraceOutputInt16(FILE *pfdebug, const char *data, int *cursor)
+pqTraceOutputInt16(FILE *pfdebug, const char *data, int *cursor, int end)
{
uint16 tmp;
int result;
+ if (!pqTraceCheckLength(2, cursor, end))
+ return 0;
memcpy(&tmp, data + *cursor, 2);
*cursor += 2;
result = (int) pg_ntoh16(tmp);
@@ -140,12 +176,16 @@ pqTraceOutputInt16(FILE *pfdebug, const char *data, int
*cursor)
* pqTraceOutputInt32: output a 4-byte integer message to the log
*
* If 'suppress' is true, print a literal NNNN instead of the actual number.
+ * Returns the integer, or 0 if the message is truncated.
*/
static int
-pqTraceOutputInt32(FILE *pfdebug, const char *data, int *cursor, bool suppress)
+pqTraceOutputInt32(FILE *pfdebug, const char *data, int *cursor, int end,
+ bool suppress)
{
int result;
+ if (!pqTraceCheckLength(4, cursor, end))
+ return 0;
memcpy(&result, data + *cursor, 4);
*cursor += 4;
result = (int) pg_ntoh32(result);
@@ -163,25 +203,28 @@ pqTraceOutputInt32(FILE *pfdebug, const char *data, int
*cursor, bool suppress)
* If 'suppress' is true, print a literal "SSSS" instead of the actual string.
*/
static void
-pqTraceOutputString(FILE *pfdebug, const char *data, int *cursor, bool
suppress)
+pqTraceOutputString(FILE *pfdebug, const char *data, int *cursor, int end,
+ bool suppress)
{
+ const char *v;
+ const char *nul;
int len;
+ /* We need at least the terminating \0 */
+ if (!pqTraceCheckLength(1, cursor, end))
+ return;
+ v = data + *cursor;
+ nul = memchr(v, '\0', end - *cursor);
+ len = nul ? nul - v : end - *cursor;
+
if (suppress)
- {
fprintf(pfdebug, " \"SSSS\"");
- *cursor += strlen(data + *cursor) + 1;
- }
else
- {
- len = fprintf(pfdebug, " \"%s\"", data + *cursor);
+ fprintf(pfdebug, " \"%.*s\"", len, v);
- /*
- * This is a null-terminated string. So add 1 after subtracting
3
- * which is the double quotes and space length from len.
- */
- *cursor += (len - 3 + 1);
- }
+ /* An unterminated string means the message is truncated */
+ if (pqTraceCheckLength(len + 1, cursor, end))
+ *cursor += len + 1;
}
/*
@@ -190,22 +233,32 @@ pqTraceOutputString(FILE *pfdebug, const char *data, int
*cursor, bool suppress)
* If 'suppress' is true, print a literal 'BBBB' instead of the actual bytes.
*/
static void
-pqTraceOutputNchar(FILE *pfdebug, int len, const char *data, int *cursor, bool
suppress)
+pqTraceOutputNchar(FILE *pfdebug, int len, const char *data, int *cursor,
+ int end, bool suppress)
{
int i,
+ n, /* number of
bytes to print */
next; /* first char not yet
printed */
- const char *v = data + *cursor;
+ const char *v;
+
+ if (!pqTraceCheckLength(0, cursor, end))
+ return;
+ v = data + *cursor;
if (suppress)
{
fprintf(pfdebug, " 'BBBB'");
- *cursor += len;
+ if (pqTraceCheckLength(len, cursor, end))
+ *cursor += len;
return;
}
+ /* If the message is shorter than that, print what there is of it */
+ n = Max(Min(len, end - *cursor), 0);
+
fprintf(pfdebug, " \'");
- for (next = i = 0; i < len; ++i)
+ for (next = i = 0; i < n; ++i)
{
if (isprint((unsigned char) v[i]))
continue;
@@ -216,11 +269,12 @@ pqTraceOutputNchar(FILE *pfdebug, int len, const char
*data, int *cursor, bool s
next = i + 1;
}
}
- if (next < len)
- fwrite(v + next, 1, len - next, pfdebug);
+ if (next < n)
+ fwrite(v + next, 1, n - next, pfdebug);
fprintf(pfdebug, "\'");
- *cursor += len;
+ if (pqTraceCheckLength(len, cursor, end))
+ *cursor += len;
}
/*
@@ -228,97 +282,98 @@ pqTraceOutputNchar(FILE *pfdebug, int len, const char
*data, int *cursor, bool s
*/
static void
-pqTraceOutput_NotificationResponse(FILE *f, const char *message, int *cursor,
bool regress)
+pqTraceOutput_NotificationResponse(FILE *f, const char *message, int *cursor,
int end, bool regress)
{
fprintf(f, "NotificationResponse\t");
- pqTraceOutputInt32(f, message, cursor, regress);
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
-pqTraceOutput_Bind(FILE *f, const char *message, int *cursor)
+pqTraceOutput_Bind(FILE *f, const char *message, int *cursor, int end)
{
int nparams;
fprintf(f, "Bind\t");
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputString(f, message, cursor, false);
- nparams = pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputString(f, message, cursor, end, false);
+ nparams = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nparams; i++)
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
- nparams = pqTraceOutputInt16(f, message, cursor);
+ nparams = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nparams; i++)
{
int nbytes;
- nbytes = pqTraceOutputInt32(f, message, cursor, false);
+ nbytes = pqTraceOutputInt32(f, message, cursor, end, false);
if (nbytes == -1)
continue;
- pqTraceOutputNchar(f, nbytes, message, cursor, false);
+ pqTraceOutputNchar(f, nbytes, message, cursor, end, false);
}
- nparams = pqTraceOutputInt16(f, message, cursor);
+ nparams = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nparams; i++)
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
}
static void
-pqTraceOutput_Close(FILE *f, const char *message, int *cursor)
+pqTraceOutput_Close(FILE *f, const char *message, int *cursor, int end)
{
fprintf(f, "Close\t");
- pqTraceOutputByte1(f, message, cursor);
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputByte1(f, message, cursor, end);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
-pqTraceOutput_CommandComplete(FILE *f, const char *message, int *cursor)
+pqTraceOutput_CommandComplete(FILE *f, const char *message, int *cursor, int
end)
{
fprintf(f, "CommandComplete\t");
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
-pqTraceOutput_CopyData(FILE *f, const char *message, int *cursor, int length,
+pqTraceOutput_CopyData(FILE *f, const char *message, int *cursor, int end,
bool suppress)
{
fprintf(f, "CopyData\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message, cursor, suppress);
+ pqTraceOutputNchar(f, end - *cursor, message, cursor, end, suppress);
}
static void
-pqTraceOutput_DataRow(FILE *f, const char *message, int *cursor)
+pqTraceOutput_DataRow(FILE *f, const char *message, int *cursor, int end)
{
int nfields;
int len;
int i;
fprintf(f, "DataRow\t");
- nfields = pqTraceOutputInt16(f, message, cursor);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (i = 0; i < nfields; i++)
{
- len = pqTraceOutputInt32(f, message, cursor, false);
- if (len == -1)
+ len = pqTraceOutputInt32(f, message, cursor, end, false);
+ /* libpq takes any negative length as NULL */
+ if (len < 0)
continue;
- pqTraceOutputNchar(f, len, message, cursor, false);
+ pqTraceOutputNchar(f, len, message, cursor, end, false);
}
}
static void
-pqTraceOutput_Describe(FILE *f, const char *message, int *cursor)
+pqTraceOutput_Describe(FILE *f, const char *message, int *cursor, int end)
{
fprintf(f, "Describe\t");
- pqTraceOutputByte1(f, message, cursor);
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputByte1(f, message, cursor, end);
+ pqTraceOutputString(f, message, cursor, end, false);
}
/* shared code NoticeResponse / ErrorResponse */
static void
pqTraceOutputNR(FILE *f, const char *type, const char *message, int *cursor,
- bool regress)
+ int end, bool regress)
{
fprintf(f, "%s\t", type);
for (;;)
@@ -326,167 +381,168 @@ pqTraceOutputNR(FILE *f, const char *type, const char
*message, int *cursor,
char field;
bool suppress;
- pqTraceOutputByte1(f, message, cursor);
- field = message[*cursor - 1];
+ field = pqTraceOutputByte1(f, message, cursor, end);
if (field == '\0')
break;
suppress = regress && (field == 'L' || field == 'F' || field ==
'R');
- pqTraceOutputString(f, message, cursor, suppress);
+ pqTraceOutputString(f, message, cursor, end, suppress);
}
}
static void
-pqTraceOutput_ErrorResponse(FILE *f, const char *message, int *cursor, bool
regress)
+pqTraceOutput_ErrorResponse(FILE *f, const char *message, int *cursor, int
end, bool regress)
{
- pqTraceOutputNR(f, "ErrorResponse", message, cursor, regress);
+ pqTraceOutputNR(f, "ErrorResponse", message, cursor, end, regress);
}
static void
-pqTraceOutput_NoticeResponse(FILE *f, const char *message, int *cursor, bool
regress)
+pqTraceOutput_NoticeResponse(FILE *f, const char *message, int *cursor, int
end, bool regress)
{
- pqTraceOutputNR(f, "NoticeResponse", message, cursor, regress);
+ pqTraceOutputNR(f, "NoticeResponse", message, cursor, end, regress);
}
static void
-pqTraceOutput_Execute(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutput_Execute(FILE *f, const char *message, int *cursor, int end, bool
regress)
{
fprintf(f, "Execute\t");
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputInt32(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputInt32(f, message, cursor, end, false);
}
static void
-pqTraceOutput_CopyFail(FILE *f, const char *message, int *cursor)
+pqTraceOutput_CopyFail(FILE *f, const char *message, int *cursor, int end)
{
fprintf(f, "CopyFail\t");
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
pqTraceOutput_GSSResponse(FILE *f, const char *message, int *cursor,
- int length, bool regress)
+ int end, bool regress)
{
fprintf(f, "GSSResponse\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message, cursor, regress);
+ pqTraceOutputNchar(f, end - *cursor, message, cursor, end, regress);
}
static void
-pqTraceOutput_PasswordMessage(FILE *f, const char *message, int *cursor)
+pqTraceOutput_PasswordMessage(FILE *f, const char *message, int *cursor, int
end)
{
fprintf(f, "PasswordMessage\t");
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
-pqTraceOutput_SASLInitialResponse(FILE *f, const char *message, int *cursor,
+pqTraceOutput_SASLInitialResponse(FILE *f, const char *message, int *cursor,
int end,
bool regress)
{
int initialResponse;
fprintf(f, "SASLInitialResponse\t");
- pqTraceOutputString(f, message, cursor, false);
- initialResponse = pqTraceOutputInt32(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
+ initialResponse = pqTraceOutputInt32(f, message, cursor, end, false);
if (initialResponse != -1)
- pqTraceOutputNchar(f, initialResponse, message, cursor,
regress);
+ pqTraceOutputNchar(f, initialResponse, message, cursor, end,
regress);
}
static void
pqTraceOutput_SASLResponse(FILE *f, const char *message, int *cursor,
- int length, bool regress)
+ int end, bool regress)
{
fprintf(f, "SASLResponse\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message, cursor, regress);
+ pqTraceOutputNchar(f, end - *cursor, message, cursor, end, regress);
}
static void
-pqTraceOutput_FunctionCall(FILE *f, const char *message, int *cursor, bool
regress)
+pqTraceOutput_FunctionCall(FILE *f, const char *message, int *cursor, int end,
bool regress)
{
int nfields;
int nbytes;
fprintf(f, "FunctionCall\t");
- pqTraceOutputInt32(f, message, cursor, regress);
- nfields = pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
- nfields = pqTraceOutputInt16(f, message, cursor);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
{
- nbytes = pqTraceOutputInt32(f, message, cursor, false);
+ nbytes = pqTraceOutputInt32(f, message, cursor, end, false);
if (nbytes == -1)
continue;
- pqTraceOutputNchar(f, nbytes, message, cursor, false);
+ pqTraceOutputNchar(f, nbytes, message, cursor, end, false);
}
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
}
static void
-pqTraceOutput_CopyInResponse(FILE *f, const char *message, int *cursor)
+pqTraceOutput_CopyInResponse(FILE *f, const char *message, int *cursor, int
end)
{
int nfields;
fprintf(f, "CopyInResponse\t");
- pqTraceOutputByte1(f, message, cursor);
- nfields = pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputByte1(f, message, cursor, end);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
}
static void
-pqTraceOutput_CopyOutResponse(FILE *f, const char *message, int *cursor)
+pqTraceOutput_CopyOutResponse(FILE *f, const char *message, int *cursor, int
end)
{
int nfields;
fprintf(f, "CopyOutResponse\t");
- pqTraceOutputByte1(f, message, cursor);
- nfields = pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputByte1(f, message, cursor, end);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputInt16(f, message, cursor, end);
}
static void
-pqTraceOutput_BackendKeyData(FILE *f, const char *message, int *cursor, int
length,
+pqTraceOutput_BackendKeyData(FILE *f, const char *message, int *cursor, int
end,
bool regress)
{
fprintf(f, "BackendKeyData\t");
- pqTraceOutputInt32(f, message, cursor, regress);
- pqTraceOutputNchar(f, length - *cursor + 1, message, cursor, regress);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
+ pqTraceOutputNchar(f, end - *cursor, message, cursor, end, regress);
}
static void
-pqTraceOutput_Parse(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutput_Parse(FILE *f, const char *message, int *cursor, int end, bool
regress)
{
int nparams;
fprintf(f, "Parse\t");
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputString(f, message, cursor, false);
- nparams = pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputString(f, message, cursor, end, false);
+ nparams = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nparams; i++)
- pqTraceOutputInt32(f, message, cursor, regress);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
}
static void
-pqTraceOutput_Query(FILE *f, const char *message, int *cursor)
+pqTraceOutput_Query(FILE *f, const char *message, int *cursor, int end)
{
fprintf(f, "Query\t");
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
pqTraceOutput_Authentication(FILE *f, const char *message, int *cursor,
- int length, bool
suppress)
+ int end, bool suppress)
{
int authType = 0;
+ if (!pqTraceCheckLength(4, cursor, end))
+ return;
memcpy(&authType, message + *cursor, 4);
authType = (int) pg_ntoh32(authType);
*cursor += 4;
@@ -509,7 +565,7 @@ pqTraceOutput_Authentication(FILE *f, const char *message,
int *cursor,
break;
case AUTH_REQ_GSS_CONT:
fprintf(f, "AuthenticationGSSContinue\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message,
cursor,
+ pqTraceOutputNchar(f, end - *cursor, message, cursor,
end,
suppress);
break;
case AUTH_REQ_SSPI:
@@ -517,18 +573,18 @@ pqTraceOutput_Authentication(FILE *f, const char
*message, int *cursor,
break;
case AUTH_REQ_SASL:
fprintf(f, "AuthenticationSASL\t");
- while (message[*cursor] != '\0')
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputString(f, message, cursor, false);
+ while (*cursor < end && message[*cursor] != '\0')
+ pqTraceOutputString(f, message, cursor, end,
false);
+ pqTraceOutputString(f, message, cursor, end, false);
break;
case AUTH_REQ_SASL_CONT:
fprintf(f, "AuthenticationSASLContinue\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message,
cursor,
+ pqTraceOutputNchar(f, end - *cursor, message, cursor,
end,
suppress);
break;
case AUTH_REQ_SASL_FIN:
fprintf(f, "AuthenticationSASLFinal\t");
- pqTraceOutputNchar(f, length - *cursor + 1, message,
cursor,
+ pqTraceOutputNchar(f, end - *cursor, message, cursor,
end,
suppress);
break;
default:
@@ -537,95 +593,104 @@ pqTraceOutput_Authentication(FILE *f, const char
*message, int *cursor,
}
static void
-pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
+pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor, int
end)
{
fprintf(f, "ParameterStatus\t");
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
static void
-pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor,
bool regress)
+pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor,
int end, bool regress)
{
int nfields;
fprintf(f, "ParameterDescription\t");
- nfields = pqTraceOutputInt16(f, message, cursor);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
- pqTraceOutputInt32(f, message, cursor, regress);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
}
static void
-pqTraceOutput_RowDescription(FILE *f, const char *message, int *cursor, bool
regress)
+pqTraceOutput_RowDescription(FILE *f, const char *message, int *cursor, int
end, bool regress)
{
int nfields;
fprintf(f, "RowDescription\t");
- nfields = pqTraceOutputInt16(f, message, cursor);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
for (int i = 0; i < nfields; i++)
{
- pqTraceOutputString(f, message, cursor, false);
- pqTraceOutputInt32(f, message, cursor, regress);
- pqTraceOutputInt16(f, message, cursor);
- pqTraceOutputInt32(f, message, cursor, regress);
- pqTraceOutputInt16(f, message, cursor);
- pqTraceOutputInt32(f, message, cursor, false);
- pqTraceOutputInt16(f, message, cursor);
+ pqTraceOutputString(f, message, cursor, end, false);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
+ pqTraceOutputInt16(f, message, cursor, end);
+ pqTraceOutputInt32(f, message, cursor, end, regress);
+ pqTraceOutputInt16(f, message, cursor, end);
+ pqTraceOutputInt32(f, message, cursor, end, false);
+ pqTraceOutputInt16(f, message, cursor, end);
}
}
static void
-pqTraceOutput_NegotiateProtocolVersion(FILE *f, const char *message, int
*cursor)
+pqTraceOutput_NegotiateProtocolVersion(FILE *f, const char *message, int
*cursor, int end)
{
int nparams;
fprintf(f, "NegotiateProtocolVersion\t");
- pqTraceOutputInt32(f, message, cursor, false);
- nparams = pqTraceOutputInt32(f, message, cursor, false);
- for (int i = 0; i < nparams; i++)
+ pqTraceOutputInt32(f, message, cursor, end, false);
+ nparams = pqTraceOutputInt32(f, message, cursor, end, false);
+
+ /* The count is 32 bits wide, so stop once the message is truncated */
+ for (int i = 0; i < nparams && *cursor <= end; i++)
{
- pqTraceOutputString(f, message, cursor, false);
+ pqTraceOutputString(f, message, cursor, end, false);
}
}
static void
-pqTraceOutput_FunctionCallResponse(FILE *f, const char *message, int *cursor)
+pqTraceOutput_FunctionCallResponse(FILE *f, const char *message, int *cursor,
int end)
{
int len;
fprintf(f, "FunctionCallResponse\t");
- len = pqTraceOutputInt32(f, message, cursor, false);
+ len = pqTraceOutputInt32(f, message, cursor, end, false);
if (len != -1)
- pqTraceOutputNchar(f, len, message, cursor, false);
+ pqTraceOutputNchar(f, len, message, cursor, end, false);
}
static void
-pqTraceOutput_CopyBothResponse(FILE *f, const char *message, int *cursor, int
length)
+pqTraceOutput_CopyBothResponse(FILE *f, const char *message, int *cursor, int
end)
{
+ int nfields;
+
fprintf(f, "CopyBothResponse\t");
- pqTraceOutputByte1(f, message, cursor);
+ pqTraceOutputByte1(f, message, cursor, end);
+ nfields = pqTraceOutputInt16(f, message, cursor, end);
- while (length > *cursor)
- pqTraceOutputInt16(f, message, cursor);
+ for (int i = 0; i < nfields; i++)
+ pqTraceOutputInt16(f, message, cursor, end);
}
static void
-pqTraceOutput_ReadyForQuery(FILE *f, const char *message, int *cursor)
+pqTraceOutput_ReadyForQuery(FILE *f, const char *message, int *cursor, int end)
{
fprintf(f, "ReadyForQuery\t");
- pqTraceOutputByte1(f, message, cursor);
+ pqTraceOutputByte1(f, message, cursor, end);
}
/*
* Print the given message to the trace output stream.
+ *
+ * The caller must have checked that the whole message, as long as its length
+ * word says, is in the buffer.
*/
void
pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
{
char id;
int length;
+ int end;
char *prefix = toServer ? "F" : "B";
int logCursor = 0;
bool regress;
@@ -645,6 +710,10 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
length = (int) pg_ntoh32(length);
logCursor += 4;
+ /* The length doesn't count the type byte, but the cursor does */
+ Assert(length >= 4);
+ end = length + 1;
+
/*
* In regress mode, suppress the length of ErrorResponse and
* NoticeResponse. The F (file name), L (line number) and R (routine
@@ -671,10 +740,10 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
/* No message content */
break;
case PqMsg_NotificationResponse:
- pqTraceOutput_NotificationResponse(conn->Pfdebug,
message, &logCursor, regress);
+ pqTraceOutput_NotificationResponse(conn->Pfdebug,
message, &logCursor, end, regress);
break;
case PqMsg_Bind:
- pqTraceOutput_Bind(conn->Pfdebug, message, &logCursor);
+ pqTraceOutput_Bind(conn->Pfdebug, message, &logCursor,
end);
break;
case PqMsg_CopyDone:
fprintf(conn->Pfdebug, "CopyDone");
@@ -684,32 +753,32 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
/* Close(F) and CommandComplete(B) use the same
identifier. */
Assert(PqMsg_Close == PqMsg_CommandComplete);
if (toServer)
- pqTraceOutput_Close(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_Close(conn->Pfdebug, message,
&logCursor, end);
else
- pqTraceOutput_CommandComplete(conn->Pfdebug,
message, &logCursor);
+ pqTraceOutput_CommandComplete(conn->Pfdebug,
message, &logCursor, end);
break;
case PqMsg_CopyData:
pqTraceOutput_CopyData(conn->Pfdebug, message,
&logCursor,
- length,
regress);
+ end,
regress);
break;
case PqMsg_Describe:
/* Describe(F) and DataRow(B) use the same identifier.
*/
Assert(PqMsg_Describe == PqMsg_DataRow);
if (toServer)
- pqTraceOutput_Describe(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_Describe(conn->Pfdebug, message,
&logCursor, end);
else
- pqTraceOutput_DataRow(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_DataRow(conn->Pfdebug, message,
&logCursor, end);
break;
case PqMsg_Execute:
/* Execute(F) and ErrorResponse(B) use the same
identifier. */
Assert(PqMsg_Execute == PqMsg_ErrorResponse);
if (toServer)
- pqTraceOutput_Execute(conn->Pfdebug, message,
&logCursor, regress);
+ pqTraceOutput_Execute(conn->Pfdebug, message,
&logCursor, end, regress);
else
- pqTraceOutput_ErrorResponse(conn->Pfdebug,
message, &logCursor, regress);
+ pqTraceOutput_ErrorResponse(conn->Pfdebug,
message, &logCursor, end, regress);
break;
case PqMsg_CopyFail:
- pqTraceOutput_CopyFail(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_CopyFail(conn->Pfdebug, message,
&logCursor, end);
break;
case PqMsg_GSSResponse:
Assert(PqMsg_GSSResponse == PqMsg_PasswordMessage);
@@ -724,19 +793,19 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
{
case AUTH_RESPONSE_GSS:
pqTraceOutput_GSSResponse(conn->Pfdebug, message,
-
&logCursor, length, regress);
+
&logCursor, end, regress);
break;
case AUTH_RESPONSE_PASSWORD:
pqTraceOutput_PasswordMessage(conn->Pfdebug, message,
-
&logCursor);
+
&logCursor, end);
break;
case AUTH_RESPONSE_SASL_INITIAL:
pqTraceOutput_SASLInitialResponse(conn->Pfdebug, message,
-
&logCursor, regress);
+
&logCursor, end, regress);
break;
case AUTH_RESPONSE_SASL:
pqTraceOutput_SASLResponse(conn->Pfdebug, message,
-
&logCursor, length, regress);
+
&logCursor, end, regress);
break;
default:
fprintf(conn->Pfdebug,
"UnknownAuthenticationResponse");
@@ -745,10 +814,10 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
conn->current_auth_response = '\0';
break;
case PqMsg_FunctionCall:
- pqTraceOutput_FunctionCall(conn->Pfdebug, message,
&logCursor, regress);
+ pqTraceOutput_FunctionCall(conn->Pfdebug, message,
&logCursor, end, regress);
break;
case PqMsg_CopyInResponse:
- pqTraceOutput_CopyInResponse(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_CopyInResponse(conn->Pfdebug, message,
&logCursor, end);
break;
case PqMsg_Flush:
/* Flush(F) and CopyOutResponse(B) use the same
identifier */
@@ -756,7 +825,7 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
if (toServer)
fprintf(conn->Pfdebug, "Flush"); /* no
message content */
else
- pqTraceOutput_CopyOutResponse(conn->Pfdebug,
message, &logCursor);
+ pqTraceOutput_CopyOutResponse(conn->Pfdebug,
message, &logCursor, end);
break;
case PqMsg_EmptyQueryResponse:
fprintf(conn->Pfdebug, "EmptyQueryResponse");
@@ -764,24 +833,24 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
break;
case PqMsg_BackendKeyData:
pqTraceOutput_BackendKeyData(conn->Pfdebug, message,
&logCursor,
-
length, regress);
+
end, regress);
break;
case PqMsg_NoData:
fprintf(conn->Pfdebug, "NoData");
/* No message content */
break;
case PqMsg_NoticeResponse:
- pqTraceOutput_NoticeResponse(conn->Pfdebug, message,
&logCursor, regress);
+ pqTraceOutput_NoticeResponse(conn->Pfdebug, message,
&logCursor, end, regress);
break;
case PqMsg_Parse:
- pqTraceOutput_Parse(conn->Pfdebug, message, &logCursor,
regress);
+ pqTraceOutput_Parse(conn->Pfdebug, message, &logCursor,
end, regress);
break;
case PqMsg_Query:
- pqTraceOutput_Query(conn->Pfdebug, message, &logCursor);
+ pqTraceOutput_Query(conn->Pfdebug, message, &logCursor,
end);
break;
case PqMsg_AuthenticationRequest:
pqTraceOutput_Authentication(conn->Pfdebug, message,
&logCursor,
-
length, regress);
+
end, regress);
break;
case PqMsg_PortalSuspended:
fprintf(conn->Pfdebug, "PortalSuspended");
@@ -793,29 +862,29 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
if (toServer)
fprintf(conn->Pfdebug, "Sync"); /* no message
content */
else
- pqTraceOutput_ParameterStatus(conn->Pfdebug,
message, &logCursor);
+ pqTraceOutput_ParameterStatus(conn->Pfdebug,
message, &logCursor, end);
break;
case PqMsg_ParameterDescription:
- pqTraceOutput_ParameterDescription(conn->Pfdebug,
message, &logCursor, regress);
+ pqTraceOutput_ParameterDescription(conn->Pfdebug,
message, &logCursor, end, regress);
break;
case PqMsg_RowDescription:
- pqTraceOutput_RowDescription(conn->Pfdebug, message,
&logCursor, regress);
+ pqTraceOutput_RowDescription(conn->Pfdebug, message,
&logCursor, end, regress);
break;
case PqMsg_NegotiateProtocolVersion:
- pqTraceOutput_NegotiateProtocolVersion(conn->Pfdebug,
message, &logCursor);
+ pqTraceOutput_NegotiateProtocolVersion(conn->Pfdebug,
message, &logCursor, end);
break;
case PqMsg_FunctionCallResponse:
- pqTraceOutput_FunctionCallResponse(conn->Pfdebug,
message, &logCursor);
+ pqTraceOutput_FunctionCallResponse(conn->Pfdebug,
message, &logCursor, end);
break;
case PqMsg_CopyBothResponse:
- pqTraceOutput_CopyBothResponse(conn->Pfdebug, message,
&logCursor, length);
+ pqTraceOutput_CopyBothResponse(conn->Pfdebug, message,
&logCursor, end);
break;
case PqMsg_Terminate:
fprintf(conn->Pfdebug, "Terminate");
/* No message content */
break;
case PqMsg_ReadyForQuery:
- pqTraceOutput_ReadyForQuery(conn->Pfdebug, message,
&logCursor);
+ pqTraceOutput_ReadyForQuery(conn->Pfdebug, message,
&logCursor, end);
break;
default:
fprintf(conn->Pfdebug, "Unknown message: %02x", id);
@@ -827,9 +896,14 @@ pqTraceOutputMessage(PGconn *conn, const char *message,
bool toServer)
/*
* Verify the printing routine did it right. Note that the one-byte
* message identifier is not included in the length, but our cursor does
- * include it.
+ * include it. A cursor past the end means that the message's contents
+ * didn't fit in its length, and printing stopped at the end.
*/
- if (logCursor - 1 != length)
+ if (logCursor > end)
+ fprintf(conn->Pfdebug,
+ "mismatched message length: contents exceed
length %d\n",
+ length);
+ else if (logCursor - 1 != length)
fprintf(conn->Pfdebug,
"mismatched message length: consumed %d,
expected %d\n",
logCursor - 1, length);
@@ -875,34 +949,37 @@ pqTraceOutputNoTypeByteMessage(PGconn *conn, const char
*message)
if (version == CANCEL_REQUEST_CODE && length >= 16)
{
fprintf(conn->Pfdebug, "CancelRequest\t");
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- pqTraceOutputInt32(conn->Pfdebug, message, &logCursor, regress);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ pqTraceOutputInt32(conn->Pfdebug, message, &logCursor, length,
+ regress);
pqTraceOutputNchar(conn->Pfdebug, length - logCursor, message,
- &logCursor, regress);
+ &logCursor, length, regress);
}
else if (version == NEGOTIATE_SSL_CODE)
{
fprintf(conn->Pfdebug, "SSLRequest\t");
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
}
else if (version == NEGOTIATE_GSS_CODE)
{
fprintf(conn->Pfdebug, "GSSENCRequest\t");
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
}
else
{
fprintf(conn->Pfdebug, "StartupMessage\t");
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- pqTraceOutputInt16(conn->Pfdebug, message, &logCursor);
- while (message[logCursor] != '\0')
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ pqTraceOutputInt16(conn->Pfdebug, message, &logCursor, length);
+ while (logCursor < length && message[logCursor] != '\0')
{
/* XXX should we suppress anything in regress mode? */
- pqTraceOutputString(conn->Pfdebug, message, &logCursor,
false);
- pqTraceOutputString(conn->Pfdebug, message, &logCursor,
false);
+ pqTraceOutputString(conn->Pfdebug, message, &logCursor,
length,
+ false);
+ pqTraceOutputString(conn->Pfdebug, message, &logCursor,
length,
+ false);
}
}
From 0b8ba06701c5b24eaebabeb79f62c980ddea49f5 Mon Sep 17 00:00:00 2001
From: Ana Bobillo <[email protected]>
Date: Thu, 22 Oct 2026 20:56:23 +0200
Subject: [PATCH v1 2/3] libpq: Don't trace a pre-3.0 error message as a
protocol 3 message
From: Ana Bobillo <[email protected]>
A server that answers the startup packet with an error message from
before protocol 3.0 (servers before version 14 did so for errors raised
before they read the startup packet) sends no length word. Since
b8b3f861fbd sent every message received during connection startup to
the tracer, such a message was traced as if its first four characters
were its length, and the tracer read past its end. Don't trace it.
---
src/interfaces/libpq/fe-connect.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/interfaces/libpq/fe-connect.c
b/src/interfaces/libpq/fe-connect.c
index 811cb0f2a50..9a1250b6a72 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4088,8 +4088,13 @@ keep_going:
/* We will come back to here until there is
/* We'll come back when there
is more data */
return PGRES_POLLING_READING;
}
- /* OK, we read the message; mark data
consumed */
- pqParseDone(conn, conn->inCursor);
+
+ /*
+ * OK, we read the message; mark data
consumed. Don't
+ * trace it: it has no length word, so
the tracer can't
+ * tell where it ends.
+ */
+ conn->inStart = conn->inCursor;
/*
* Before 7.2, the postmaster didn't
always end its
From 59024932caa82f5ec8711a7749279563cccbbd4c Mon Sep 17 00:00:00 2001
From: Ana Bobillo <[email protected]>
Date: Thu, 22 Oct 2026 20:56:23 +0200
Subject: [PATCH v1 3/3] libpq: Add a test of tracing malformed protocol
messages
From: Ana Bobillo <[email protected]>
libpq_trace_regress includes fe-trace.c, as test-oauth-curl.c includes
oauth-curl.c, and traces one message given on its command line, in a
buffer of exactly the message's size, so that AddressSanitizer or a
similar tool catches any read past the end of the message.
t/007_trace.pl runs it over well-formed messages and over messages
whose contents don't fit in their length, and checks what is printed.
---
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq/t/007_trace.pl | 137 ++++++++++++++++++
src/interfaces/libpq/test/.gitignore | 1 +
src/interfaces/libpq/test/Makefile | 7 +-
.../libpq/test/libpq_trace_regress.c | 91 ++++++++++++
src/interfaces/libpq/test/meson.build | 21 +++
6 files changed, 256 insertions(+), 2 deletions(-)
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..39a36471445 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -161,6 +161,7 @@ tests += {
't/004_load_balance_dns.pl',
't/005_negotiate_encryption.pl',
't/006_service.pl',
+ 't/007_trace.pl',
],
'env': {
'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/007_trace.pl
b/src/interfaces/libpq/t/007_trace.pl
new file mode 100644
index 00000000000..e218813b3d8
--- /dev/null
+++ b/src/interfaces/libpq/t/007_trace.pl
@@ -0,0 +1,137 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test libpq's protocol message tracing with messages whose contents don't
+# fit in their length. The tracer must stop at the end of the message, and
+# report it, instead of reading past it.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+use IPC::Run;
+
+# Each test is the direction (F or B), the message type, its contents in hex,
+# and the expected trace output. libpq_trace_regress fills in the length.
+my @tests = (
+ # Well-formed messages
+ [ 'B', 'D', '0002 00000003 616263 ffffffff', qq{B\t17\tDataRow\t 2 3
'abc' -1} ],
+ [ 'B', 'N', '53 4e4f5449434500 00', qq{B\t13\tNoticeResponse\t S
"NOTICE" \\x00} ],
+ [ 'B', 'd', '68656c6c6f0a', qq{B\t10\tCopyData\t 'hello\\x0a'} ],
+ [ 'B', 'K', '00000001 0203', qq{B\t10\tBackendKeyData\t 1 '\\x02\\x03'}
],
+ [ 'F', 'C', '53 666f6f00', qq{F\t9\tClose\t S "foo"} ],
+ [
+ 'B', 'R', '0000000a 534352414d2d5348412d32353600 00',
+ qq{B\t23\tAuthenticationSASL\t "SCRAM-SHA-256" ""}
+ ],
+ [
+ 'B', 'v', '00030000 00000001 5f707100',
+ qq{B\t16\tNegotiateProtocolVersion\t 196608 1 "_pq"}
+ ],
+ [ 'B', 'W', '00 0001 0000', qq{B\t9\tCopyBothResponse\t \\x00 1 0} ],
+
+ # A negative field length other than -1 is a NULL too, as libpq reads it
+ [ 'B', 'D', '0002 fffffffe 00000001 62', qq{B\t15\tDataRow\t 2 -2 1
'b'} ],
+
+ # Contents left over at the end are still reported
+ [
+ 'B', 'Z', '4900',
+ qq{B\t6\tReadyForQuery\t I\nmismatched message length: consumed
5, expected 6}
+ ],
+
+ # Fields that run past the end of the message
+ [
+ 'B', 'D', '0001 000003e8 616263',
+ qq{B\t13\tDataRow\t 1 1000 'abc'\nmismatched message length:
contents exceed length 13}
+ ],
+ [
+ 'B', 'D', '0001 7fffffff 61',
+ qq{B\t11\tDataRow\t 1 2147483647 'a'\nmismatched message
length: contents exceed length 11}
+ ],
+ [
+ 'B', 'V', '00000004 6162',
+ qq{B\t10\tFunctionCallResponse\t 4 'ab'\nmismatched message
length: contents exceed length 10}
+ ],
+ [
+ 'B', 'V', 'fffffffb',
+ qq{B\t8\tFunctionCallResponse\t -5 ''\nmismatched message
length: contents exceed length 8}
+ ],
+
+ # A frontend message type is printed as such, even coming from the
server
+ [
+ 'B', 'B', '00 00 0001 0000 0001 000000ff 41',
+ qq{B\t17\tBind\t "" "" 1 0 1 255 'A'\nmismatched message
length: contents exceed length 17}
+ ],
+
+ # Counts of more fields than the message holds
+ [
+ 'B', 'D', '0003 00000001 61',
+ qq{B\t11\tDataRow\t 3 1 'a'\nmismatched message length:
contents exceed length 11}
+ ],
+ [
+ 'B', 'T', '0001 6100 00000000 0000',
+ qq{B\t14\tRowDescription\t 1 "a" 0 0\nmismatched message
length: contents exceed length 14}
+ ],
+ [
+ 'B', 't', '0002 00000017',
+ qq{B\t10\tParameterDescription\t 2 23\nmismatched message
length: contents exceed length 10}
+ ],
+ [
+ 'B', 'G', '00 0003 0000',
+ qq{B\t9\tCopyInResponse\t \\x00 3 0\nmismatched message length:
contents exceed length 9}
+ ],
+ [
+ 'B', 'v', '00030000 7fffffff 5f707100',
+ qq{B\t16\tNegotiateProtocolVersion\t 196608 2147483647
"_pq"\nmismatched message length: contents exceed length 16}
+ ],
+ [
+ 'B', 'W', '00 0003 0000',
+ qq{B\t9\tCopyBothResponse\t \\x00 3 0\nmismatched message
length: contents exceed length 9}
+ ],
+ [
+ 'B', 'W', '00 0001 00',
+ qq{B\t8\tCopyBothResponse\t \\x00 1\nmismatched message length:
contents exceed length 8}
+ ],
+
+ # Strings and lists without their terminating zero byte
+ [
+ 'B', 'C', '53454c454354',
+ qq{B\t10\tCommandComplete\t "SELECT"\nmismatched message
length: contents exceed length 10}
+ ],
+ [
+ 'B', 'S', '6100 62',
+ qq{B\t7\tParameterStatus\t "a" "b"\nmismatched message length:
contents exceed length 7}
+ ],
+ [
+ 'B', 'A', '00000001 6300',
+ qq{B\t10\tNotificationResponse\t 1 "c"\nmismatched message
length: contents exceed length 10}
+ ],
+ [
+ 'B', 'E', '53 4552524f5200',
+ qq{B\t11\tErrorResponse\t S "ERROR"\nmismatched message length:
contents exceed length 11}
+ ],
+ [
+ 'B', 'R', '0000000a 534352414d2d5348412d32353600',
+ qq{B\t22\tAuthenticationSASL\t "SCRAM-SHA-256"\nmismatched
message length: contents exceed length 22}
+ ],
+ [
+ 'B', 'R', '',
+ qq{B\t4\t\nmismatched message length: contents exceed length 4}
+ ]);
+
+foreach my $test (@tests)
+{
+ my ($direction, $type, $data, $expected) = @$test;
+ my $name = "pqTraceOutputMessage: $direction '$type' $data";
+
+ my ($stdout, $stderr);
+ my $result = IPC::Run::run [ 'libpq_trace_regress', $direction, $type,
$data ],
+ '>' => \$stdout,
+ '2>' => \$stderr;
+ chomp($stdout);
+
+ ok($result, "$name: exit status");
+ is($stdout, $expected, "$name: trace");
+ is($stderr, '', "$name: no errors");
+}
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore
b/src/interfaces/libpq/test/.gitignore
index 6ba78adb678..04a6b3144e0 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
/libpq_testclient
+/libpq_trace_regress
/libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile
b/src/interfaces/libpq/test/Makefile
index 4e17ec15141..6abb82f7b7e 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -11,14 +11,17 @@ ifeq ($(PORTNAME), win32)
LDFLAGS += -lws2_32
endif
-override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port
-I$(top_srcdir)/src/port $(CPPFLAGS)
LDFLAGS_INTERNAL += $(libpq_pgport)
-PROGS = libpq_testclient libpq_uri_regress
+PROGS = libpq_testclient libpq_trace_regress libpq_uri_regress
all: $(PROGS)
$(PROGS): $(WIN32RES)
+# libpq_trace_regress.c includes fe-trace.c
+libpq_trace_regress.o: $(libpq_srcdir)/fe-trace.c
+
clean distclean:
rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/libpq_trace_regress.c
b/src/interfaces/libpq/test/libpq_trace_regress.c
new file mode 100644
index 00000000000..1f5999bb6d6
--- /dev/null
+++ b/src/interfaces/libpq/test/libpq_trace_regress.c
@@ -0,0 +1,91 @@
+/*
+ * libpq_trace_regress.c
+ * A test program for libpq's protocol message tracing
+ *
+ * This #includes fe-trace.c, so that it can call pqTraceOutputMessage()
+ * directly. It traces one message, built from its arguments, in a buffer of
+ * exactly its size, so that a memory checker such as AddressSanitizer catches
+ * any read past its end. Without one, the test still checks the output.
+ *
+ * Usage: libpq_trace_regress F|B TYPE HEXDATA
+ *
+ * F or B says whether the message goes to the server or comes from it, TYPE
+ * is its type byte and HEXDATA its contents in hex (spaces are ignored). The
+ * length word is filled in to match the contents.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/test/libpq_trace_regress.c
+ */
+
+#include "fe-trace.c"
+
+static int
+hexval(char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ return -1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ PGconn conn;
+ const char *hex;
+ char *message;
+ int len = 0;
+ uint32 n32;
+
+ if (argc != 4 || (strcmp(argv[1], "F") != 0 && strcmp(argv[1], "B") !=
0) ||
+ strlen(argv[2]) != 1)
+ {
+ fprintf(stderr, "usage: %s F|B TYPE HEXDATA\n", argv[0]);
+ return 1;
+ }
+
+ /* Type byte, length word, then the contents */
+ message = malloc(1 + 4 + strlen(argv[3]) / 2);
+ if (message == NULL)
+ {
+ fprintf(stderr, "out of memory\n");
+ return 1;
+ }
+ for (hex = argv[3]; *hex; hex++)
+ {
+ if (*hex == ' ')
+ continue;
+ if (hexval(hex[0]) < 0 || hex[1] == '\0' || hexval(hex[1]) < 0)
+ {
+ fprintf(stderr, "invalid hex data \"%s\"\n", argv[3]);
+ return 1;
+ }
+ message[1 + 4 + len++] = hexval(hex[0]) * 16 + hexval(hex[1]);
+ hex++;
+ }
+ message[0] = argv[2][0];
+ n32 = pg_hton32(4 + len);
+ memcpy(message + 1, &n32, 4);
+
+ /* Make the buffer exactly as long as the message */
+ message = realloc(message, 1 + 4 + len);
+ if (message == NULL)
+ {
+ fprintf(stderr, "out of memory\n");
+ return 1;
+ }
+
+ memset(&conn, 0, sizeof(conn));
+ conn.Pfdebug = stdout;
+ conn.traceFlags = PQTRACE_SUPPRESS_TIMESTAMPS;
+
+ pqTraceOutputMessage(&conn, message, argv[1][0] == 'F');
+
+ free(message);
+ return 0;
+}
diff --git a/src/interfaces/libpq/test/meson.build
b/src/interfaces/libpq/test/meson.build
index e203486615c..52d27d5a457 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -39,4 +39,25 @@ libpq_test_deps += executable('libpq_testclient',
}
)
+libpq_trace_regress_sources = files(
+ 'libpq_trace_regress.c',
+)
+
+if host_system == 'windows'
+ libpq_trace_regress_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'libpq_trace_regress',
+ '--FILEDESC', 'libpq test program',])
+endif
+
+# This includes fe-trace.c, so it needs libpq's internal headers (and the
+# headers they include) but not the library
+libpq_test_deps += executable('libpq_trace_regress',
+ libpq_trace_regress_sources,
+ include_directories: [libpq_inc],
+ dependencies: [frontend_no_fe_utils_code, ssl, gssapi],
+ kwargs: default_bin_args + {
+ 'install': false,
+ }
+)
+
testprep_targets += libpq_test_deps