Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w621V-003sXS-0F for pgsql-bugs@arkaria.postgresql.org; Fri, 27 Mar 2026 07:54:29 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1w621T-008I5O-26 for pgsql-bugs@arkaria.postgresql.org; Fri, 27 Mar 2026 07:54:28 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w621S-008I5E-38 for pgsql-bugs@lists.postgresql.org; Fri, 27 Mar 2026 07:54:27 +0000 Received: from mail-pj1-x1030.google.com ([2607:f8b0:4864:20::1030]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1w621R-00000001HaU-07Y0 for pgsql-bugs@lists.postgresql.org; Fri, 27 Mar 2026 07:54:26 +0000 Received: by mail-pj1-x1030.google.com with SMTP id 98e67ed59e1d1-356337f058aso1107260a91.2 for ; Fri, 27 Mar 2026 00:54:24 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1774598064; cv=none; d=google.com; s=arc-20240605; b=RTJeVUkHbUnBtAoYHOnWNKD8AVYWQ+lmOiUuE7OGVSxDwYLPB07tIz9UEuPmW/jRaC 1bNSpf9pELPGQwpfrSyUrMgnKCDAagiwcVOdkWiX9WoF/07P6569oOThCNJzfSB5N084 FMfrRJGABW6MCJAMBylY/cN/oz49D8ZiHPz4Nw4gcLrG5BYMqUc2VG+4j+nkYIl/5geI HvGgL6lf6pHHaPilf12UDDwr5mpxfjpE4uTviECsCGejjwnCsDpLPZbGKgRkN98XVMj+ 9/AQ3kLolG0UT78GNnqWFgnJND11ei6FMvGZe1QqVaXPWpkH014WLuIVj8gOjEPEYV3I brMA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=to:subject:message-id:date:from:mime-version:dkim-signature; bh=zRunX7h0vlgEDfgEKFO7V657FyfdwOHYGW3fEpUY7kw=; fh=/gQe77b11iMZdcPj/nJr/Ghqi6rQp5FPrPbdO93bmOA=; b=XLT55Wbzhtx+tO4orE8qYZ4ybjgShUf1K9eh1gQM+RR+H4Rbj5mIc/efU/XzFE9dXS +wD54pJDnqgxa2cP3SUQaaPNxZpzTgCeriWkwe+7+Bt9syP2Ui3umNk+dUvjL5rKHDA5 E6dSrNb05Yd48tF+pJj7Awnv8JnQofLZGJqeSqNifKZCxN9+bmGfXNRGw5G1/2tGNSWI JcCue0tA1PPKRkFhw0BxJrD3GApmD+yyGSUVpWQ03T7YXj5mQ/bfWLVuUMnQ/GxnG9fB 9zBtNdsHXkrfBzB3u7JWnZ6Ie4MWwFfuNGAUn+t9m489PPMceqQIDvM2GCJdRPlJLuyV /NXg==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yugabyte.com; s=google; t=1774598064; x=1775202864; darn=lists.postgresql.org; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=zRunX7h0vlgEDfgEKFO7V657FyfdwOHYGW3fEpUY7kw=; b=e1dXUsNL3zpRdtf/8MznE2BkELgSabldWQxTXPx1iXM+Jj6TqKH6fWW5VxtDT93ZJN 0QjFdEeoBvu+UI/r6+ZNfj07zaKHU9c4upLdZ4KRNckXfOFOd8G7CvVBnvs75c7PHwod 3dxWTu6rARuZt9VuJwRRE+TvztPjUHyifBDhg= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1774598064; x=1775202864; h=to:subject:message-id:date:from:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=zRunX7h0vlgEDfgEKFO7V657FyfdwOHYGW3fEpUY7kw=; b=fXu7boMGFJoPh0IV75EFIwW+vRGLP/wkO6XZkmyOE2FTQDJlQg5hYAkhZdeOd3JVSD TNRT72LhtNhgX5U7KQQDo2b7/4REHV+J3H7gqCnbl7q8vJs9s6594acXWWN+VUnD5dF8 DapDYq03/jsEdVdao29tZ4JbTXoKLTXXWNmEcFE7PZ1uPn87s4HyGtCj9Sadj/WmTGOD xPpLCepuaA8hQtFAchp56izYxowxaVbjjjufg4IxlMSjLRzZNyc+YtbsngGdOUojliPr PDfXsqcYrULuHoMSBF/ovf1CCAC2AshWQt2KmNwpsUNRbDHiHJ+y/Emp0NpCc6YU3DLM IxzA== X-Gm-Message-State: AOJu0YzCojMcmdJF8zZmSSbHwE3//SM/ZD2N+kANMmkYaDcXoe2TMT+i PPbx3Pfp4o1rr1KL6+Vo7njSrTIS8YpBTNgL+gQGG/dIQgRqJwQuAj785gdzbQKqWhnofvtaDsU SON7OWkIffTDkCDHs5AvhEQCHT9xh74exDRpEUu6B+lSHuK4QFo0RzRWZow== X-Gm-Gg: ATEYQzy2TnJ1LPTWID+upcj1Z/2ObxtxUDwDuLUjbuPQj15HiL0snlimhOCOWBS+o3c trvuVJaY48BSB/Hjdcvt1EapD7IbgzAtZRnJ6xWQoQ6WyDO8wQrns3ZY5tJJD2mAGCj8Gc0k9st wiplnbFqbTtlrpmLIu6vHW305l52ZUTHAfly4eZCOgti6r1YHbTKTNubzYB49XEU/5Php0FHJSm XEA3U6FfmOvsl2xzxszznY2/tONR6qPznj2MUBWOWSbaYsq7XH/v9rDnOuAyUvtUORSRTzzHDj1 ZUL2eZcd X-Received: by 2002:a17:90b:1642:b0:359:fdc0:4621 with SMTP id 98e67ed59e1d1-35c2ffa8f44mr1778177a91.11.1774598063346; Fri, 27 Mar 2026 00:54:23 -0700 (PDT) MIME-Version: 1.0 From: Gaurav Singh Date: Fri, 27 Mar 2026 13:24:11 +0530 X-Gm-Features: AQROBzD6HTKr1egBrD1f5s7lrPFbbmtqOvfLXqKhWggzF8XlPH9T5m4XKyS7uyI Message-ID: Subject: Memory leak in pg_stat_statements when qtext file contains invalid encoding To: pgsql-bugs@lists.postgresql.org Content-Type: multipart/alternative; boundary="0000000000002f6554064dfccf16" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --0000000000002f6554064dfccf16 Content-Type: text/plain; charset="UTF-8" I've found a memory leak in contrib/pg_stat_statements that occurs when the query text file (pgss_query_texts.stat) contains an invalid byte sequence. Each call to pg_stat_statements leaks the entire malloc'd file buffer and fails to release the LWLock.PostgreSQL version: Discovered against PG 15.12, verified also present in PG 18 (HEAD as of 2026-03-26). The code path is unchanged between versions.Platform: macOS 15.7.3 (aarch64).Although the cause of corruption inpgss_query_texts.statis unknown, we have seen this twice. Similar cases were found online.Eg:ERROR: invalid byte sequence for encoding "UTF8": 0x00 in pg_stat_statements - Database Administrators Stack Exchange .How to reproduce: -- 1. Enable pg_stat_statements CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SELECT pg_stat_statements_reset(); -- 2. Populate with many unique queries to make the qtext file large. -- Each CTE name is padded to 63 chars (NAMEDATALEN - 1) and repeated -- in self-joins so the normalized text is ~300 bytes per entry. -- Run this for i = 1..2000: WITH q_1___________________________________________ AS (SELECT 1) SELECT * FROM q_1___________________________________________ a1, q_1___________________________________________ a2, q_1___________________________________________ a3; -- (repeat with q_2, q_3, ... q_2000) # 3. Corrupt the qtext file with a null byte. DATA_DIR=$(psql -t -A -c "SHOW data_directory;") printf '\x00' | dd of="$DATA_DIR/pg_stat_tmp/pgss_query_texts.stat" \ bs=1 seek=500 count=1 conv=notrunc 2>/dev/null -- 4. Verify corruption causes the expected error: SELECT count(*) FROM pg_stat_statements; -- ERROR: invalid byte sequence for encoding "UTF8": 0x00 # 5. In a single psql session, run the query 2000 times and monitor # the backend's RSS: BACKEND_PID=$(psql -t -A -c "SELECT pg_backend_pid();") for i in $(seq 1 2000); do psql -c "SELECT count(*) FROM pg_stat_statements;" 2>/dev/null done & # Monitor in another terminal: watch -n 2 "ps -o rss= -p $BACKEND_PID" Output I got:RSS grows linearly with each failing query. With a ~600 KB qtext file and 2000 iterations, the backend's RSS grew by approximately 1.2 GB: Time(s) RSS (KB) RSS (MB) 0s 68864 67 4s 95712 93 8s 156736 153 12s 232256 226 ... 38s 756800 739 42s 907264 885 46s 1052864 1028 50s 1193024 1164 54s 1281280 1251 Leak per error is approximately equal to the qtext file size (~600 KB), confirming the file buffer is leaked on every call. Output I expected:RSS should remain approximately constant. Each call should either succeed or fail cleanly without leaking memory. The LWLock should always be released.Root cause: - In pg_stat_statements_internal(), the function acquires pgss->lock and may malloc a file buffer via qtext_load_file(). - Later, pg_any_to_server() is called inside the hash iteration loop. - If the qtext file contains an invalid encoding, pg_any_to_server calls ereport(ERROR) which longjmps out of the function. - The cleanup code at the bottom of the function is never reached. LWLockRelease(pgss->lock); if (qbuffer) free(qbuffer); On every subsequent call, the malloc'd buffer (the entire file contents) is leaked, and the LWLock release is also skipped.Proposed fix:Wrap the hash iteration loop in PG_TRY/PG_FINALLY so that the lock release and buffer free happen even on the error path: PG_TRY(); { hash_seq_init(&hash_seq, pgss_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { /* ... existing loop body unchanged ... */ } } PG_FINALLY(); { LWLockRelease(pgss->lock); if (qbuffer) free(qbuffer); } PG_END_TRY(); Gaurav Singh --0000000000002f6554064dfccf16 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
I've found=C2=A0a memory leak in=C2=A0contrib/pg_stat_statements=C2=A0that=C2=A0o= ccurs when the query=C2=A0text file (pgss_query_texts.stat) contains an<= /span>=C2=A0invalid byte=C2=A0sequence. Each=C2=A0call=C2=A0to=C2= =A0pg_stat_statement= s=C2=A0leaks the=C2=A0entire=C2=A0malloc'd f= ile=C2=A0buffer=C2=A0and=C2=A0fails=C2=A0to release<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">=C2= =A0the=C2=A0LWLock.Postgre<= /span>SQL version:=C2=A0Discovered=C2=A0against=C2=A0P<= /span>G 15.12, verified=C2=A0also=C2=A0present=C2=A0in PG 1<= /span>8=C2=A0(HEAD=C2=A0as=C2=A0of 2026-03-26)<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">. Th= e code=C2=A0path=C2=A0is=C2=A0unchanged=C2=A0between= =C2= =A0versions.Platform:<= /span>=C2=A0macOS=C2=A015.7.3 (aarch64).Although the cause of corruption in= pgss_query_texts.statis unknown,=C2= =A0<= font face=3D"arial, sans-serif" style=3D"">we have seen this twice. Similar= cases were found online.Eg:= ERROR= : invalid byte sequence for encoding "UTF8": 0x00 in pg_stat_stat= ements - Database Administrators Stack Exchange.How=C2=A0to reproduce:
=
= -- 1. Enable pg_stat_statements
C= REATE EXTEN= SION IF NOT EXISTS pg_stat_statements;
SELECT pg_stat_statements_reset();
-- 2. Populate with = many unique queries to make the qtext file large.
<= div class=3D"gmail-slim-code-line" style=3D"font-family:inherit;display:fle= x;min-height:20px">
-- Each CTE name is p= added to 63 chars (NAMEDATALEN - 1) and repeated
-- in self-joins so th= e normalized text is ~300 bytes per entry.
-- Run this for i =3D 1..200= 0:
WITH q_1_______________________________= ____________ AS (SELECT 1)
SELECT= *= FROM q_1_________= __________________________________ a1,
q_1___________________________________________ a2,
q_1______________________________= _____________ a3;
-- (repeat with q_2, q_3, ... q_2000)
<= /div>
=
# 3. Corrupt the qtext file with a null byte.
DATA_DIR=3D$(psql -t -A -c "SHOW data_= directory;")
printf '\x00' | dd o= f=3D"$DATA_DIR/pg_stat_tmp/pgss_query_texts.stat&= quot; \
bs=3D1 seek=3D= 500 = count=3D1 conv=3Dnotrunc 2>/dev/null
<= div class=3D"gmail-slider" style=3D"background:repeat rgba(34,34,34,0.07);w= idth:10px;height:92px">
-- 4. Verify corruption causes the expected er= ror:
SELECT count(*) FROM pg_stat_statements;
= -- ERROR: invalid byte sequence for encoding "UTF8": 0x00=
<= /div>
=
# 5. In a single psql session, r= un the query 2000 times and monitor
<= span class=3D"gmail-mtk17 gmail-mtki" style=3D"font-family:inherit;color:rg= ba(34,34,34,0.37);font-style:italic"># the backend's RSS:
BACKEND_PID=3D$(psql -t -A -c "SELEC= T pg_backend_pid();")
for i in $(seq 1 20= 00); do
psql -c "SELECT count(*) F= ROM pg_stat_statements;" 2>/dev/null
=
done &
# Monitor in another terminal:
watch -n 2 "ps -o rss=3D -p $BACKE= ND_PID"
Output=C2=A0I=C2=A0got:= RSS=C2=A0grows=C2=A0linearly with each=C2=A0failing<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">=C2= =A0query. With=C2=A0a=C2=A0~600=C2=A0KB=C2=A0qtext = file and=C2=A02000 iterations, the backend's RSS grew= =C2=A0by=C2=A0approximately=C2=A01.2 GB:
Time(s) RSS (KB) = RSS (MB)
0s 68864 67
4s 95712 93
8s 156736 153
12s 232= 256 226
...
38s 756800 739
42s = 907264 885
46s 1052864 102= 8
50s 1193024 1164
54s 1281280 1251
<= /div>
Leak=C2=A0per=C2=A0error=C2=A0is=C2=A0app= roximately=C2=A0equal=C2=A0to the qtext file size (~600= =C2=A0KB), confirming the file=C2=A0buffer=C2=A0is leaked=C2=A0every=C2=A0call.
Output=C2=A0I expect= ed:RSS=C2=A0shou= ld=C2=A0remain=C2=A0approximately=C2=A0constant. Each= =C2=A0call=C2=A0should either=C2=A0succeed=C2=A0or=C2=A0fai= l cleanly without leaking memory. The LWLock should always=C2= =A0be released.Root=C2=A0cause:=
  • In=C2=A0pg_stat_s= tatements_internal(), the=C2=A0function=C2=A0acq= uires=C2=A0p= gss->lock=C2=A0and=C2=A0may=C2=A0malloc=C2=A0file=C2=A0buffer via=C2=A0qtext_load_file().
  • Later,= =C2=A0pg_any<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">_to_= server()=C2=A0is=C2=A0called inside=C2=A0the<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">=C2= =A0hash=C2=A0iteration=C2=A0loop.
  • If=C2=A0the=C2=A0qtext file c= ontains an=C2=A0invalid encoding,=C2=A0pg_any_to_server=C2=A0callsereport(= ERROR)=C2=A0which=C2=A0longjmps out=C2=A0of the func= tion.
  • Th= e=C2=A0cleanup=C2=A0code at=C2=A0the=C2=A0bottom=C2=A0o= f the function=C2=A0is=C2=A0never reached= .=C2=A0
LWLockRelease(pgss->lock);
if (qbuffer)
= free(qbuffer);
On=C2=A0every=C2=A0subsequent=C2=A0call, the malloc'd=C2=A0buffer=C2=A0(the<= span style=3D"animation: 0.25s ease 0s 1 normal none running fade-in;">=C2= =A0entire=C2=A0file contents) is leaked, and the LWLock release= =C2=A0is also=C2=A0skipped.Proposed=C2=A0fix:Wrap=C2=A0the hash=C2=A0iteration loop=C2=A0in=C2=A0PG_TRY/PG_FI= NALLY=C2=A0so=C2=A0that the lock release=C2=A0and buff= er free happen even on the=C2=A0error path:
PG_TRY();
{
= hash_s= eq_init(&hash= _seq, pgss_hash);
while ((= entry = =3D = hash_seq_search(&hash_seq)) !=3D NULL)
<= span class=3D"gmail-mtk1" style=3D"font-family:inherit"> {=
= /* ... existing loop body unchanged ... */
}<= /div>
}
PG_FINALLY= ();
{
LWLockRelease(pgss->lock);
if (qbuffer)<= /div>
free(qbuffer);
}<= /span>
PG_END_TRY();
=

Gaurav Singh
--0000000000002f6554064dfccf16--