Hi,

Two client programs crash when the server answers an internal query with a
result set of an unexpected shape. Both are plain NULL dereferences: the code
checks part of the shape, then calls PQgetvalue() on a cell that does not
exist. (First sent to security@, Noah pointed me here since the project does
not treat these as security issues.)

0001 - libpq, fe-connect.c. PQconnectPoll gates the target_session_attrs
probes ("SHOW transaction_read_only" and "SELECT pg_is_in_recovery()") on
PQntuples(res) == 1 but never on PQnfields(res), then does
strncmp(PQgetvalue(res, 0, 0), ...). One row and zero columns gives
strncmp(NULL, ...). The patch requires PQnfields(res) == 1 too, so a bad reply
takes the existing "Something went wrong" error path and the connection
attempt fails.

0002 - pg_basebackup, receivelog.c. The TIMELINE_HISTORY shape check logs a
warning and falls through to writeTimeLineHistoryFile(PQgetvalue(res, 0, 0),
PQgetvalue(res, 0, 1)), which calls strlen() on the NULL right away. The patch
makes the check fatal (pg_log_error, PQclear, return false), matching the
other error returns in that block. It has been like this since cc8d4151172.

Tested on master at 0607aa6109 with clang 21 and -fsanitize=address,undefined
against a small fake server on loopback: both crash before the patch, both
report a clean error after it, and the well-formed control paths still work
(libpq connects, pg_receivewal writes the history file and moves on to
START_REPLICATION). Both fixes apply cleanly to the back branches as far as
I checked, but I have only run them on master.

Regards,
s1ko
From 816cfd5398488b0989bbb72a0fd9f03570185ef6 Mon Sep 17 00:00:00 2001
From: s1ko <[email protected]>
Date: Sun, 8 Nov 2026 18:35:22 +0100
Subject: [PATCH v1 1/2] libpq: check column count before reading
 target_session_attrs probe result

PQconnectPoll trusted the shape of the result set returned for the
internal "SHOW transaction_read_only" and "SELECT pg_is_in_recovery()"
queries, checking PQntuples() but not PQnfields() before calling
PQgetvalue(res, 0, 0).  A server that answers with one row and zero
columns makes PQgetvalue() return NULL, which is then passed to
strncmp() and crashes the client.

Require exactly one column as well, so that a malformed reply falls
through to the existing error path and the connection attempt fails
instead.
---
 src/interfaces/libpq/fe-connect.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9e17a8f1c6..b30585c74c 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4596,7 +4596,7 @@ keep_going:						/* We will come back to here until there is
 
 				res = PQgetResult(conn);
 				if (res && PQresultStatus(res) == PGRES_TUPLES_OK &&
-					PQntuples(res) == 1)
+					PQntuples(res) == 1 && PQnfields(res) == 1)
 				{
 					char	   *val = PQgetvalue(res, 0, 0);
 
@@ -4664,7 +4664,7 @@ keep_going:						/* We will come back to here until there is
 
 				res = PQgetResult(conn);
 				if (res && PQresultStatus(res) == PGRES_TUPLES_OK &&
-					PQntuples(res) == 1)
+					PQntuples(res) == 1 && PQnfields(res) == 1)
 				{
 					char	   *val = PQgetvalue(res, 0, 0);
 
-- 
2.53.0

From bed9d016fbeee5df08105662b92f4e1396b4e8d8 Mon Sep 17 00:00:00 2001
From: s1ko <[email protected]>
Date: Sun, 8 Nov 2026 18:35:22 +0100
Subject: [PATCH v1 2/2] pg_basebackup: fail on malformed TIMELINE_HISTORY
 response

ReceiveXlogStream() checked that the TIMELINE_HISTORY reply had one row
and two columns, but only logged a warning and then went on to call
writeTimeLineHistoryFile() with PQgetvalue() results that are NULL
whenever the check failed, crashing in strlen().

Make the check fatal: log an error, clear the result and return false,
as the surrounding error paths already do.  This has been reachable
since cc8d4151172 introduced the warning.
---
 src/bin/pg_basebackup/receivelog.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c
index 77a2b4458b..9b03fdfd24 100644
--- a/src/bin/pg_basebackup/receivelog.c
+++ b/src/bin/pg_basebackup/receivelog.c
@@ -552,8 +552,10 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream)
 			 */
 			if (PQnfields(res) != 2 || PQntuples(res) != 1)
 			{
-				pg_log_warning("unexpected response to TIMELINE_HISTORY command: got %d rows and %d fields, expected %d rows and %d fields",
-							   PQntuples(res), PQnfields(res), 1, 2);
+				pg_log_error("unexpected response to TIMELINE_HISTORY command: got %d rows and %d fields, expected %d rows and %d fields",
+							 PQntuples(res), PQnfields(res), 1, 2);
+				PQclear(res);
+				return false;
 			}
 
 			/* Write the history file to disk */
-- 
2.53.0

Reply via email to