Fixing pool_read2

From: Tatsuo Ishii <ishii(at)postgresql(dot)org>
To: pgpool-hackers(at)lists(dot)postgresql(dot)org
Cc: Emond Papegaaij <emond(dot)papegaaij(at)gmail(dot)com>
Subject: Fixing pool_read2
Date: 2026-08-17 09:31:10
Message-ID: 20260817.183110.398658681617422309.ishii@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgpool-hackers

Hi,

Recently Emond Papegaaij reported me off list that pool_read2() has a
bug and sent a patch (see below).

I agree that pool_read2() should check whether "len" argument has a
positive value. However, if pool_read2() reurns NULL when len == 0 (as
the patch instructs), quite a few regression test fail. This is
because in many places callers of pool_read2() expect it returns non
NULL if len == 0. Otherwise, they complain:

ERROR: unable to forward message to frontend
DETAIL: read from backend failed

pool_read2() is typically used to read the packet body (which is after
the packet lenth field). Even if len == 0 case (no body packet like
BindComplete), callers feel free to call palloc with 0 byte, and
memcpy with 0 byte. Fortunately palloc and memcpy are flexible enough
to accept 0 byte argument. Maybe we should teach the callers to check
len == 0 or not, but there are too many such places. For now, I would
like to keep the current behavior (allow to length == 0). Attached is
a patch to implement that way. (with slight commit message change and
indentation change in the code to follow our coding standard).

-----------------------------------------------------------------------
From 83278ae402affafdc0ff0278a76c13476345b71f Mon Sep 17 00:00:00 2001
From: Emond Papegaaij <emond(dot)papegaaij(at)topicus(dot)nl>
Date: Fri, 24 Apr 2026 23:40:34 +0200
Subject: [PATCH] Reject non-positive length in pool_read2.

pool_read2() in src/utils/pool_stream.c allocates buf2 sized to
cp->len + len, consumes that many bytes from the pending buffer,
then loops reading from the socket while len > 0. When len is zero
or negative the loop never executes a fresh read and the function
falls straight through to "return cp->buf2", handing the caller a
pointer to whatever the previous pool_read2() invocation happened
to leave in the per-connection buffer.

Callers compute len from on-the-wire length fields, e.g.
len = ntohl(hdr) - sizeof(hdr);
and several do so without bounds-checking. A backend that ships a
malformed message with a length smaller than the header (or a
negative value once cast to int32) drives pool_read2() into the
stale-buffer path. The stale bytes are then forwarded to the
frontend as ParameterStatus / ErrorResponse / etc. payloads,
leaking whatever the previous reply contained on this same
backend connection.

Add a guard at the top of pool_read2() that logs and returns NULL
when len <= 0, before any buffer setup runs. The function's
existing comment already claims a NULL return for the failure
case; this aligns the implementation with that contract for the
non-positive-len input. A separate audit of the ~40 call sites,
to make every caller treat a NULL return consistently rather than
dereference it, is deferred to a follow-up consolidation patch:
this change shuts the immediate stale-data leak without fanning
out call-site churn across release branches.
---
src/utils/pool_stream.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/utils/pool_stream.c b/src/utils/pool_stream.c
index 0a9cc3576..9bee94b3c 100644
--- a/src/utils/pool_stream.c
+++ b/src/utils/pool_stream.c
@@ -305,6 +305,8 @@ pool_read(POOL_CONNECTION *cp, void *buf, int len)
/*
* read exactly len bytes from cp
* returns buffer address on success otherwise NULL.
+* Returns NULL when len <= 0; callers must already tolerate a NULL
+* return because the read-error paths below also return NULL.
*/
char *
pool_read2(POOL_CONNECTION *cp, int len)
@@ -314,7 +316,16 @@ pool_read2(POOL_CONNECTION *cp, int len)
int alloc_size;
int consume_size;
int readlen;
- MemoryContext oldContext = SwitchToConnectionContext(cp->isbackend);
+ MemoryContext oldContext;
+
+ if (len <= 0)
+ {
+ ereport(LOG,
+ (errmsg("pool_read2: non-positive len %d, returning NULL", len)));
+ return NULL;
+ }
+
+ oldContext = SwitchToConnectionContext(cp->isbackend);

req_size = cp->len + len;

--
2.51.0
-----------------------------------------------------------------------

Regards,
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp

Attachment Content-Type Size
v2-0001-Reject-negative-read-length-in-pool_read2.patch application/octet-stream 2.5 KB

Browse pgpool-hackers by date

  From Date Subject
Next Message Koshino Taiki 2026-08-20 08:15:52 [PATCH v1] Fix hang on deferred constraint errors in pipeline mode
Previous Message Tatsuo Ishii 2026-08-17 06:27:37 Re: Convert close_idle_connection (SIGUSR1) to flag-only handler with main-loop processing.