From 463177f46b9e40c82f51dc36ec5a264ba58e20e9 Mon Sep 17 00:00:00 2001
From: Shihao Zhong <zhong950419@gmail.com>
Date: Fri, 11 Sep 2026 12:26:11 +0200
Subject: [PATCH v7 1/2] Make per-backend statistics functions respect
 statistics permissions

The per-backend statistics functions that report the details of a session
call HAS_PGSTAT_PERMISSIONS() first, so that they hide these details from
a caller that is neither a superuser, nor has privileges of
pg_read_all_stats, nor is a member of the role that owns the session.
pg_stat_get_backend_subxact(), pg_stat_get_backend_io(),
pg_stat_get_backend_wal() and pg_stat_get_backend_lock() lacked this
check.  Add it, for consistency with the sibling functions.  Like
pg_stat_activity, this also hides the statistics of processes owned by
no role, such as autovacuum workers or the WAL writer, from callers
without these privileges.

The last three look up a backend by PID and have no backend status entry
at hand, so pgstat_fetch_stat_backend_by_pid() gains an optional "userid"
output argument, next to the existing "bktype" one, returning the OID of
the role that owns the backend.

The permission rule was documented for the dynamic statistics views but
not for these functions, so state it above the per-backend function table
and on the three functions listed among the additional statistics
functions.  While on it, correct the name of the subxact_overflowed
column in the tuple descriptor built by pg_stat_get_backend_subxact().

This changes the output of existing functions for callers lacking the
required privileges, so no backpatch is done.

Author: Shihao Zhong <zhong950419@gmail.com>
Author: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://postgr.es/m/CAGRkXqTBZ+zbVuDC8xGEB6Btj61hsui5H5nGqzFBDyOXc=4bjQ@mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                | 25 +++++++++
 src/backend/utils/activity/pgstat_backend.c | 12 ++++-
 src/backend/utils/adt/pgstatfuncs.c         | 22 +++++---
 src/include/pgstat.h                        |  3 +-
 src/test/regress/expected/stats.out         | 59 +++++++++++++++++++++
 src/test/regress/sql/stats.sql              | 45 ++++++++++++++++
 6 files changed, 156 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 62dadf3e86c..78f224bf9e8 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5799,6 +5799,11 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         the background writer, the startup process and the autovacuum launcher
         as they are already visible in the <structname>pg_stat_io</structname>
         view and there is only one of each.
+       </para>
+       <para>
+        This function returns no rows unless the caller is a superuser, has
+        privileges of the <literal>pg_read_all_stats</literal> role, or is a
+        member of the role that owns the backend.
        </para></entry>
       </row>
 
@@ -5834,6 +5839,11 @@ description | Waiting for a newly initialized WAL file to reach durable storage
        <para>
         The function does not return lock statistics for the checkpointer,
         the background writer, the startup process and the autovacuum launcher.
+       </para>
+       <para>
+        This function returns no rows unless the caller is a superuser, has
+        privileges of the <literal>pg_read_all_stats</literal> role, or is a
+        member of the role that owns the backend.
        </para></entry>
       </row>
 
@@ -5853,6 +5863,11 @@ description | Waiting for a newly initialized WAL file to reach durable storage
        <para>
         The function does not return WAL statistics for the checkpointer,
         the background writer, the startup process and the autovacuum launcher.
+       </para>
+       <para>
+        This function returns NULL unless the caller is a superuser, has
+        privileges of the <literal>pg_read_all_stats</literal> role, or is a
+        member of the role that owns the backend.
        </para></entry>
       </row>
 
@@ -6211,6 +6226,16 @@ FROM pg_stat_get_backend_idset() AS backendid;
 </programlisting>
   </para>
 
+  <para>
+   These functions are security restricted in the same way as
+   <structname>pg_stat_activity</structname>.  The existence of a session and
+   its general properties, such as its session user and database, are visible
+   to all users, but the details of a session's activity are only shown if
+   the caller is a superuser, has privileges of the
+   <link linkend="predefined-role-pg-monitor"><literal>pg_read_all_stats</literal></link>
+   role, or is a member of the role that owns the session.
+  </para>
+
    <table id="monitoring-stats-backend-funcs-table">
     <title>Per-Backend Statistics Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..59bc7e699b5 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -140,10 +140,12 @@ pgstat_fetch_stat_backend(ProcNumber procNumber)
  *
  * This routine includes sanity checks to ensure that the backend exists and
  * is running.  "bktype" can be optionally defined to return the BackendType
- * of the backend whose statistics are returned.
+ * of the backend whose statistics are returned.  "userid" can be optionally
+ * defined to return the OID of the role that owns the backend, for callers
+ * that need to check whether they are allowed to report its statistics.
  */
 PgStat_Backend *
-pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype)
+pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype, Oid *userid)
 {
 	PGPROC	   *proc;
 	PgBackendStatus *beentry;
@@ -153,6 +155,8 @@ pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype)
 	proc = BackendPidGetProc(pid);
 	if (bktype)
 		*bktype = B_INVALID;
+	if (userid)
+		*userid = InvalidOid;
 
 	/* this could be an auxiliary process */
 	if (!proc)
@@ -177,6 +181,8 @@ pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype)
 
 	if (bktype)
 		*bktype = beentry->st_backendType;
+	if (userid)
+		*userid = beentry->st_userid;
 
 	/*
 	 * Retrieve the entry.  Note that "beentry" may be freed depending on the
@@ -187,6 +193,8 @@ pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype)
 	{
 		if (bktype)
 			*bktype = B_INVALID;
+		if (userid)
+			*userid = InvalidOid;
 		return NULL;
 	}
 
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..f9fc3b65b2a 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -838,7 +838,9 @@ pg_stat_get_backend_subxact(PG_FUNCTION_ARGS)
 	TupleDescFinalize(tupdesc);
 	BlessTupleDesc(tupdesc);
 
-	if ((local_beentry = pgstat_get_local_beentry_by_proc_number(procNumber)) != NULL)
+	/* Report the details of a session only to a caller allowed to see them */
+	if ((local_beentry = pgstat_get_local_beentry_by_proc_number(procNumber)) != NULL &&
+		HAS_PGSTAT_PERMISSIONS(local_beentry->backendStatus.st_userid))
 	{
 		/* Fill values and NULLs */
 		values[0] = Int32GetDatum(local_beentry->backend_subxact_count);
@@ -1673,6 +1675,7 @@ pg_stat_get_backend_io(PG_FUNCTION_ARGS)
 	ReturnSetInfo *rsinfo;
 	BackendType bktype;
 	int			pid;
+	Oid			userid;
 	PgStat_Backend *backend_stats;
 	PgStat_BktypeIO *bktype_stats;
 
@@ -1680,9 +1683,10 @@ pg_stat_get_backend_io(PG_FUNCTION_ARGS)
 	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 
 	pid = PG_GETARG_INT32(0);
-	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, &bktype);
+	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, &bktype, &userid);
 
-	if (!backend_stats)
+	/* Report the details of a session only to a caller allowed to see them */
+	if (!backend_stats || !HAS_PGSTAT_PERMISSIONS(userid))
 		return (Datum) 0;
 
 	bktype_stats = &backend_stats->io_stats;
@@ -1769,13 +1773,15 @@ Datum
 pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
 {
 	int			pid;
+	Oid			userid;
 	PgStat_Backend *backend_stats;
 	PgStat_WalCounters bktype_stats;
 
 	pid = PG_GETARG_INT32(0);
-	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL, &userid);
 
-	if (!backend_stats)
+	/* Report the details of a session only to a caller allowed to see them */
+	if (!backend_stats || !HAS_PGSTAT_PERMISSIONS(userid))
 		PG_RETURN_NULL();
 
 	bktype_stats = backend_stats->wal_counters;
@@ -1857,6 +1863,7 @@ Datum
 pg_stat_get_backend_lock(PG_FUNCTION_ARGS)
 {
 	int			pid;
+	Oid			userid;
 	ReturnSetInfo *rsinfo;
 	PgStat_Backend *backend_stats;
 
@@ -1864,9 +1871,10 @@ pg_stat_get_backend_lock(PG_FUNCTION_ARGS)
 	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 
 	pid = PG_GETARG_INT32(0);
-	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL, &userid);
 
-	if (!backend_stats)
+	/* Report the details of a session only to a caller allowed to see them */
+	if (!backend_stats || !HAS_PGSTAT_PERMISSIONS(userid))
 		return (Datum) 0;
 
 	pg_stat_lock_build_tuples(rsinfo, backend_stats->lock_stats.stats,
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 187d82c96fe..4c3dcc03df5 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -671,7 +671,8 @@ extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
 
 extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
 extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
-														BackendType *bktype);
+														BackendType *bktype,
+														Oid *userid);
 extern bool pgstat_tracks_backend_bktype(BackendType bktype);
 extern void pgstat_create_backend(ProcNumber procnum);
 
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 8b15471248b..fc1870bfd4b 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1141,6 +1141,65 @@ WHERE pg_stat_get_backend_pid(beid) = pg_backend_pid();
  t
 (1 row)
 
+-- The per-backend statistics functions report the details of a session only
+-- to a caller that is allowed to see them: a superuser, a role with
+-- privileges of pg_read_all_stats, or the role that owns the session.
+SELECT beid FROM pg_stat_get_backend_idset() beid
+WHERE pg_stat_get_backend_pid(beid) = pg_backend_pid() \gset
+SELECT current_user AS regress_stat_backend_owner \gset
+CREATE ROLE regress_stat_backend_role;
+-- a role with privileges of the role that owns this backend sees them
+GRANT :"regress_stat_backend_owner" TO regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+ subxact | io | locks | wal 
+---------+----+-------+-----
+ t       | t  | t     | t
+(1 row)
+
+RESET ROLE;
+REVOKE :"regress_stat_backend_owner" FROM regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+-- an unrelated role sees nothing
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+ subxact | io | locks | wal 
+---------+----+-------+-----
+ f       | f  | f     | f
+(1 row)
+
+RESET ROLE;
+-- but a role with privileges of pg_read_all_stats sees them again
+GRANT pg_read_all_stats TO regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+ subxact | io | locks | wal 
+---------+----+-------+-----
+ t       | t  | t     | t
+(1 row)
+
+RESET ROLE;
+DROP ROLE regress_stat_backend_role;
 -----
 -- Test that resetting stats works for reset timestamp
 -----
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 674637e172b..17c8e2f5231 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -535,6 +535,51 @@ SELECT (current_schemas(true))[1] = ('pg_temp_' || beid::text) AS match
 FROM pg_stat_get_backend_idset() beid
 WHERE pg_stat_get_backend_pid(beid) = pg_backend_pid();
 
+-- The per-backend statistics functions report the details of a session only
+-- to a caller that is allowed to see them: a superuser, a role with
+-- privileges of pg_read_all_stats, or the role that owns the session.
+SELECT beid FROM pg_stat_get_backend_idset() beid
+WHERE pg_stat_get_backend_pid(beid) = pg_backend_pid() \gset
+SELECT current_user AS regress_stat_backend_owner \gset
+CREATE ROLE regress_stat_backend_role;
+-- a role with privileges of the role that owns this backend sees them
+GRANT :"regress_stat_backend_owner" TO regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+RESET ROLE;
+REVOKE :"regress_stat_backend_owner" FROM regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+-- an unrelated role sees nothing
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+RESET ROLE;
+-- but a role with privileges of pg_read_all_stats sees them again
+GRANT pg_read_all_stats TO regress_stat_backend_role;
+SET ROLE regress_stat_backend_role;
+SELECT (SELECT subxact_count IS NOT NULL
+          FROM pg_stat_get_backend_subxact(:beid)) AS subxact,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_io(pg_backend_pid())) AS io,
+       (SELECT count(*) > 0
+          FROM pg_stat_get_backend_lock(pg_backend_pid())) AS locks,
+       (SELECT wal_records IS NOT NULL
+          FROM pg_stat_get_backend_wal(pg_backend_pid())) AS wal;
+RESET ROLE;
+DROP ROLE regress_stat_backend_role;
+
 -----
 -- Test that resetting stats works for reset timestamp
 -----
-- 
2.37.1 (Apple Git-137.1)

