public inbox for [email protected]  
help / color / mirror / Atom feed
From: Michael Paquier <[email protected]>
To: Chao Li <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Subject: Re: Fix unsafe PlannedStmt access in pg_stat_statements
Date: Tue, 12 May 2026 12:30:53 +0900
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
	<[email protected]>

On Mon, May 11, 2026 at 04:11:41PM +0800, Chao Li wrote:
> On May 11, 2026, at 16:07, Chao Li <[email protected]> wrote:
>> In pgss_ProcessUtility(), there is this comment:
>> ```
>> /*
>> * CAUTION: do not access the *pstmt data structure again below here.
>> * If it was a ROLLBACK or similar, that data structure may have been
>> * freed.  We must copy everything we still need into local variables,
>> * which we did above.
>> *
>> * For the same reason, we can't risk restoring pstmt->queryId to its
>> * former value, which'd otherwise be a good idea.
>> */
>> ```
>> 
>> The attached patch fixes this by saving pstmt->planOrigin,
>> following the same pattern already used for queryId, stmt_location,
>> and stmt_len.

Yeah, you are right.  This code should save the planOrigin but it does
not do so.

This is also pointing at a second issue that is not addressed by your
patch: there is no coverage for the assumption of this comment, or we
would have known about this issue when 3357471cf9f5 was committed.
Running the regression tests of PGSS with valgrind enabled leads to no
reports.

And that's where things got hairy on my side because it took me a
few hours before finding a case where I was able to get the following
report, where a PlannedStmt can get freed while we are in the hook
path:
==28059== Invalid read of size 4
==28059==    at 0x165E42DF: pgss_ProcessUtility (pg_stat_statements.c:1200)
==28059==    by 0x158AFCD: ProcessUtility (utility.c:524)
==28059==    by 0x1587F3B: PortalRunUtility (pquery.c:1149)
==28059==    by 0x15877E9: FillPortalStore (pquery.c:1022)

This has come down to the extended protocol and a ROLLBACK inside a
CALL procedure.  The internal ROLLBACK is able to free the PlannedStmt
in pgss_ProcessUtility() in this case, after two executions.

> Oops! Forgot the attachment.

No problem.

All that said, I am attaching an updated version of the patch, for
posterity.  I'll apply that in a bit with your fix and the test on
HEAD, after more checks.
--
Michael

From 19281d8708facd17cd65a9ab07dd7b775496a5c9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Tue, 12 May 2026 12:24:52 +0900
Subject: [PATCH] pg_stat_statements: Fix potential use-after-free of
 PlannedStmt

Blahblahblah.

Author: Chao Li
Co-authored-by: Michael Paquier
Discussion: https://postgr.es/m/
---
 .../pg_stat_statements/expected/plancache.out | 38 +++++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   |  3 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 19 ++++++++++
 3 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
index 32bf913b2861..d0796d5693c0 100644
--- a/contrib/pg_stat_statements/expected/plancache.out
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -216,6 +216,44 @@ SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_sta
 
 RESET pg_stat_statements.track;
 --
+-- Procedure with internal ROLLBACK and the extended query protocol.
+-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal
+-- ROLLBACK.
+--
+CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$
+BEGIN
+  ROLLBACK;
+END;
+$$ LANGUAGE plpgsql;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+CALL rollback_proc($1) \parse stmt_rollback
+\bind_named stmt_rollback 1 \g
+ a 
+---
+ 1
+(1 row)
+
+\bind_named stmt_rollback 2 \g
+ a 
+---
+ 2
+(1 row)
+
+SELECT calls, query FROM pg_stat_statements
+  WHERE query LIKE '%rollback_proc%'
+  ORDER BY query COLLATE "C";
+ calls |         query          
+-------+------------------------
+     2 | CALL rollback_proc($1)
+(1 row)
+
+DROP PROCEDURE rollback_proc;
+--
 -- Cleanup
 --
 DROP FUNCTION select_one_func(int);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 95a5411a39d9..a2d3ab770cc6 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -1099,6 +1099,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 	int64		saved_queryId = pstmt->queryId;
 	int			saved_stmt_location = pstmt->stmt_location;
 	int			saved_stmt_len = pstmt->stmt_len;
+	PlannedStmtOrigin saved_planOrigin = pstmt->planOrigin;
 	bool		enabled = pgss_track_utility && pgss_enabled(nesting_level);
 
 	/*
@@ -1210,7 +1211,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   0,
 				   0,
-				   pstmt->planOrigin);
+				   saved_planOrigin);
 	}
 	else
 	{
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
index 160ced7add36..948d3e985180 100644
--- a/contrib/pg_stat_statements/sql/plancache.sql
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -87,6 +87,25 @@ SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_sta
 
 RESET pg_stat_statements.track;
 
+--
+-- Procedure with internal ROLLBACK and the extended query protocol.
+-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal
+-- ROLLBACK.
+--
+CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$
+BEGIN
+  ROLLBACK;
+END;
+$$ LANGUAGE plpgsql;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+CALL rollback_proc($1) \parse stmt_rollback
+\bind_named stmt_rollback 1 \g
+\bind_named stmt_rollback 2 \g
+SELECT calls, query FROM pg_stat_statements
+  WHERE query LIKE '%rollback_proc%'
+  ORDER BY query COLLATE "C";
+DROP PROCEDURE rollback_proc;
+
 --
 -- Cleanup
 --
-- 
2.54.0



Attachments:

  [text/plain] 0001-pg_stat_statements-Fix-potential-use-after-free-of-P.patch (3.5K, 2-0001-pg_stat_statements-Fix-potential-use-after-free-of-P.patch)
  download | inline diff:
From 19281d8708facd17cd65a9ab07dd7b775496a5c9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Tue, 12 May 2026 12:24:52 +0900
Subject: [PATCH] pg_stat_statements: Fix potential use-after-free of
 PlannedStmt

Blahblahblah.

Author: Chao Li
Co-authored-by: Michael Paquier
Discussion: https://postgr.es/m/
---
 .../pg_stat_statements/expected/plancache.out | 38 +++++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   |  3 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 19 ++++++++++
 3 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
index 32bf913b2861..d0796d5693c0 100644
--- a/contrib/pg_stat_statements/expected/plancache.out
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -216,6 +216,44 @@ SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_sta
 
 RESET pg_stat_statements.track;
 --
+-- Procedure with internal ROLLBACK and the extended query protocol.
+-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal
+-- ROLLBACK.
+--
+CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$
+BEGIN
+  ROLLBACK;
+END;
+$$ LANGUAGE plpgsql;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+CALL rollback_proc($1) \parse stmt_rollback
+\bind_named stmt_rollback 1 \g
+ a 
+---
+ 1
+(1 row)
+
+\bind_named stmt_rollback 2 \g
+ a 
+---
+ 2
+(1 row)
+
+SELECT calls, query FROM pg_stat_statements
+  WHERE query LIKE '%rollback_proc%'
+  ORDER BY query COLLATE "C";
+ calls |         query          
+-------+------------------------
+     2 | CALL rollback_proc($1)
+(1 row)
+
+DROP PROCEDURE rollback_proc;
+--
 -- Cleanup
 --
 DROP FUNCTION select_one_func(int);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 95a5411a39d9..a2d3ab770cc6 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -1099,6 +1099,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 	int64		saved_queryId = pstmt->queryId;
 	int			saved_stmt_location = pstmt->stmt_location;
 	int			saved_stmt_len = pstmt->stmt_len;
+	PlannedStmtOrigin saved_planOrigin = pstmt->planOrigin;
 	bool		enabled = pgss_track_utility && pgss_enabled(nesting_level);
 
 	/*
@@ -1210,7 +1211,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   0,
 				   0,
-				   pstmt->planOrigin);
+				   saved_planOrigin);
 	}
 	else
 	{
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
index 160ced7add36..948d3e985180 100644
--- a/contrib/pg_stat_statements/sql/plancache.sql
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -87,6 +87,25 @@ SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_sta
 
 RESET pg_stat_statements.track;
 
+--
+-- Procedure with internal ROLLBACK and the extended query protocol.
+-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal
+-- ROLLBACK.
+--
+CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$
+BEGIN
+  ROLLBACK;
+END;
+$$ LANGUAGE plpgsql;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+CALL rollback_proc($1) \parse stmt_rollback
+\bind_named stmt_rollback 1 \g
+\bind_named stmt_rollback 2 \g
+SELECT calls, query FROM pg_stat_statements
+  WHERE query LIKE '%rollback_proc%'
+  ORDER BY query COLLATE "C";
+DROP PROCEDURE rollback_proc;
+
 --
 -- Cleanup
 --
-- 
2.54.0



  [application/pgp-signature] signature.asc (833B, 3-signature.asc)
  download

view thread (8+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected]
  Subject: Re: Fix unsafe PlannedStmt access in pg_stat_statements
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox