agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v10 1/2] Key PGSTAT_KIND_RELATION by relfile locator
452+ messages / 3 participants
[nested] [flat]

* [PATCH v10 1/2] Key PGSTAT_KIND_RELATION by relfile locator
@ 2025-10-01 09:45  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2025-10-01 09:45 UTC (permalink / raw)

This patch changes the key used for the PGSTAT_KIND_RELATION statistic kind.
Instead of the relation oid, it now relies on:

- dboid (linked to RelFileLocator's dbOid)
- objoid which is the result of a new macro (namely RelFileLocatorToPgStatObjid())
that computes an objoid based on the RelFileLocator's spcOid and the
RelFileLocator's relNumber.

This is possible as, since b14e9ce7d55c, the objoid is now uint64 and spcOid
and relNumber are 32 bits.

That will allow us to add new stats (add writes counters) and ensure that some
counters (n_dead_tup and friends) are replicated.

The patch introduces pgstat_reloid_to_relfilelocator() to 1) avoid calling
RelationIdGetRelation() to get the relfilelocator based on the relation oid
and 2) handle the partitioned table case.

Please note that:

- when running pg_stat_have_stats('relation',...) we now need to be connected
to the database that hosts the relation. As pg_stat_have_stats() is not
documented publicly, then the changes done in 029_stats_restart.pl look
enough.

- this patch does not handle rewrites so some tests are failing. It's only
intent is to ease the review and should not be pushed without being
merged with the following patch that handles the rewrites.

- it can be used to test that stats are incremented correctly and that we're
able to retrieve them as long as rewrites are not involved.
---
 src/backend/postmaster/autovacuum.c          |  17 +-
 src/backend/utils/activity/pgstat_relation.c | 236 ++++++++++++++++---
 src/backend/utils/adt/pgstatfuncs.c          |  22 +-
 src/include/catalog/pg_tablespace.dat        |   4 +
 src/include/catalog/pg_tablespace.h          |   8 +
 src/include/pgstat.h                         |  15 +-
 src/include/utils/pgstat_internal.h          |   1 +
 src/test/recovery/t/029_stats_restart.pl     |  40 ++--
 8 files changed, 271 insertions(+), 72 deletions(-)
   6.1% src/backend/postmaster/
  61.6% src/backend/utils/activity/
   5.1% src/backend/utils/adt/
   3.2% src/include/catalog/
   5.6% src/include/
  18.1% src/test/recovery/t/

diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1bd3924e35e..a11174b25ad 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2014,12 +2014,16 @@ do_autovacuum(void)
 		bool		dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
+		RelFileLocator locator;
 
 		if (classForm->relkind != RELKIND_RELATION &&
 			classForm->relkind != RELKIND_MATVIEW)
 			continue;
 
 		relid = classForm->oid;
+		locator.dbOid = classForm->relisshared ? InvalidOid : MyDatabaseId;
+		locator.spcOid = classForm->reltablespace;
+		locator.relNumber = classForm->relfilenode;
 
 		/*
 		 * Check if it is a temp table (presumably, of some other backend's).
@@ -2048,8 +2052,7 @@ do_autovacuum(void)
 
 		/* Fetch reloptions and the pgstat entry for this table */
 		relopts = extract_autovac_opts(tuple, pg_class_desc);
-		tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
-												  relid);
+		tabentry = pgstat_fetch_stat_tabentry_by_locator(locator);
 
 		/* Check if it needs vacuum or analyze */
 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
@@ -2114,6 +2117,7 @@ do_autovacuum(void)
 		bool		dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
+		RelFileLocator locator;
 
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
@@ -2122,6 +2126,9 @@ do_autovacuum(void)
 			continue;
 
 		relid = classForm->oid;
+		locator.dbOid = classForm->relisshared ? InvalidOid : MyDatabaseId;
+		locator.spcOid = classForm->reltablespace;
+		locator.relNumber = classForm->relfilenode;
 
 		/*
 		 * fetch reloptions -- if this toast table does not have them, try the
@@ -2141,8 +2148,7 @@ do_autovacuum(void)
 		}
 
 		/* Fetch the pgstat entry for this table */
-		tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
-												  relid);
+		tabentry = pgstat_fetch_stat_tabentry_by_locator(locator);
 
 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
 								  effective_multixact_freeze_max_age,
@@ -2939,8 +2945,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
 	PgStat_StatTabEntry *tabentry;
 
 	/* fetch the pgstat table entry */
-	tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
-											  relid);
+	tabentry = pgstat_fetch_stat_tabentry_ext(relid);
 
 	relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
 							  effective_multixact_freeze_max_age,
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 55a10c299db..a267e93f8be 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,17 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "catalog/pg_tablespace.h"
+#include "storage/lmgr.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/relmapper.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -36,13 +41,12 @@ typedef struct TwoPhasePgStatRecord
 	PgStat_Counter inserted_pre_truncdrop;
 	PgStat_Counter updated_pre_truncdrop;
 	PgStat_Counter deleted_pre_truncdrop;
-	Oid			id;				/* table's OID */
-	bool		shared;			/* is it a shared catalog? */
+	RelFileLocator locator;		/* table's rd_locator */
 	bool		truncdropped;	/* was the relation truncated/dropped? */
 } TwoPhasePgStatRecord;
 
 
-static PgStat_TableStatus *pgstat_prep_relation_pending(Oid rel_id, bool isshared);
+static PgStat_TableStatus *pgstat_prep_relation_pending(RelFileLocator locator);
 static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_level);
 static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info);
 static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop);
@@ -60,8 +64,7 @@ pgstat_copy_relation_stats(Relation dst, Relation src)
 	PgStatShared_Relation *dstshstats;
 	PgStat_EntryRef *dst_ref;
 
-	srcstats = pgstat_fetch_stat_tabentry_ext(src->rd_rel->relisshared,
-											  RelationGetRelid(src));
+	srcstats = pgstat_fetch_stat_tabentry_ext(RelationGetRelid(src));
 	if (!srcstats)
 		return;
 
@@ -94,8 +97,10 @@ pgstat_init_relation(Relation rel)
 
 	/*
 	 * We only count stats for relations with storage and partitioned tables
+	 * and we don't count stats generated during a rewrite.
 	 */
-	if (!RELKIND_HAS_STORAGE(relkind) && relkind != RELKIND_PARTITIONED_TABLE)
+	if ((!RELKIND_HAS_STORAGE(relkind) && relkind != RELKIND_PARTITIONED_TABLE) ||
+		OidIsValid(rel->rd_rel->relrewrite))
 	{
 		rel->pgstat_enabled = false;
 		rel->pgstat_info = NULL;
@@ -130,12 +135,37 @@ pgstat_init_relation(Relation rel)
 void
 pgstat_assoc_relation(Relation rel)
 {
+	RelFileLocator locator;
+
 	Assert(rel->pgstat_enabled);
 	Assert(rel->pgstat_info == NULL);
 
+	/*
+	 * Don't associate stats for relations without storage and non partitioned
+	 * tables.
+	 */
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		return;
+
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		locator = rel->rd_locator;
+	else
+	{
+		/*
+		 * Partitioned tables don't have storage, so construct a synthetic
+		 * locator for statistics tracking. Use a reserved pseudo tablespace
+		 * OID that cannot conflict with real tablespaces, and the relation
+		 * OID as relNumber. This ensures no collision with regular relations
+		 * even after OID wraparound.
+		 */
+		locator.dbOid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
+		locator.spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+		locator.relNumber = rel->rd_id;
+	}
+
 	/* Else find or make the PgStat_TableStatus entry, and update link */
-	rel->pgstat_info = pgstat_prep_relation_pending(RelationGetRelid(rel),
-													rel->rd_rel->relisshared);
+	rel->pgstat_info = pgstat_prep_relation_pending(locator);
 
 	/* don't allow link a stats to multiple relcache entries */
 	Assert(rel->pgstat_info->relation == NULL);
@@ -167,9 +197,13 @@ pgstat_unlink_relation(Relation rel)
 void
 pgstat_create_relation(Relation rel)
 {
+	/* don't track stats for relations without storage */
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+		return;
+
 	pgstat_create_transactional(PGSTAT_KIND_RELATION,
-								rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId,
-								RelationGetRelid(rel));
+								rel->rd_locator.dbOid,
+								RelFileLocatorToPgStatObjid(rel->rd_locator));
 }
 
 /*
@@ -181,9 +215,13 @@ pgstat_drop_relation(Relation rel)
 	int			nest_level = GetCurrentTransactionNestLevel();
 	PgStat_TableStatus *pgstat_info;
 
+	/* don't track stats for relations without storage */
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+		return;
+
 	pgstat_drop_transactional(PGSTAT_KIND_RELATION,
-							  rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId,
-							  RelationGetRelid(rel));
+							  rel->rd_locator.dbOid,
+							  RelFileLocatorToPgStatObjid(rel->rd_locator));
 
 	if (!pgstat_should_count_relation(rel))
 		return;
@@ -213,20 +251,23 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
-	Oid			dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 	PgStat_Counter elapsedtime;
+	RelFileLocator locator;
 
 	if (!pgstat_track_counts)
 		return;
 
+	locator = rel->rd_locator;
+
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
 	elapsedtime = TimestampDifferenceMilliseconds(starttime, ts);
 
 	/* block acquiring lock for the same reason as pgstat_report_autovac() */
-	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid,
-											RelationGetRelid(rel), false);
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, locator.dbOid,
+											RelFileLocatorToPgStatObjid(locator),
+											false);
 
 	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
 	tabentry = &shtabentry->stats;
@@ -285,9 +326,9 @@ pgstat_report_analyze(Relation rel,
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
-	Oid			dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 	PgStat_Counter elapsedtime;
+	RelFileLocator locator;
 
 	if (!pgstat_track_counts)
 		return;
@@ -325,9 +366,25 @@ pgstat_report_analyze(Relation rel,
 	ts = GetCurrentTimestamp();
 	elapsedtime = TimestampDifferenceMilliseconds(starttime, ts);
 
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		locator = rel->rd_locator;
+	else
+	{
+		/*
+		 * Partitioned tables don't have storage, so construct a synthetic
+		 * locator for statistics tracking. Use a reserved pseudo tablespace
+		 * OID that cannot conflict with real tablespaces, and the relation
+		 * OID as relNumber. This ensures no collision with regular relations
+		 * even after OID wraparound.
+		 */
+		locator.dbOid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
+		locator.spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+		locator.relNumber = rel->rd_id;
+	}
 	/* block acquiring lock for the same reason as pgstat_report_autovac() */
-	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid,
-											RelationGetRelid(rel),
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											locator.dbOid,
+											RelFileLocatorToPgStatObjid(locator),
 											false);
 	/* can't get dropped while accessed */
 	Assert(entry_ref != NULL && entry_ref->shared_stats != NULL);
@@ -468,7 +525,16 @@ pgstat_update_heap_dead_tuples(Relation rel, int delta)
 PgStat_StatTabEntry *
 pgstat_fetch_stat_tabentry(Oid relid)
 {
-	return pgstat_fetch_stat_tabentry_ext(IsSharedRelation(relid), relid);
+	return pgstat_fetch_stat_tabentry_ext(relid);
+}
+
+PgStat_StatTabEntry *
+pgstat_fetch_stat_tabentry_by_locator(RelFileLocator locator)
+{
+	return (PgStat_StatTabEntry *) pgstat_fetch_entry(
+													  PGSTAT_KIND_RELATION,
+													  locator.dbOid,
+													  RelFileLocatorToPgStatObjid(locator));
 }
 
 /*
@@ -476,12 +542,14 @@ pgstat_fetch_stat_tabentry(Oid relid)
  * whether the to-be-accessed table is a shared relation or not.
  */
 PgStat_StatTabEntry *
-pgstat_fetch_stat_tabentry_ext(bool shared, Oid reloid)
+pgstat_fetch_stat_tabentry_ext(Oid reloid)
 {
-	Oid			dboid = (shared ? InvalidOid : MyDatabaseId);
+	RelFileLocator locator;
 
-	return (PgStat_StatTabEntry *)
-		pgstat_fetch_entry(PGSTAT_KIND_RELATION, dboid, reloid);
+	if (!pgstat_reloid_to_relfilelocator(reloid, &locator))
+		return NULL;
+
+	return pgstat_fetch_stat_tabentry_by_locator(locator);
 }
 
 /*
@@ -503,14 +571,17 @@ find_tabstat_entry(Oid rel_id)
 	PgStat_TableXactStatus *trans;
 	PgStat_TableStatus *tabentry = NULL;
 	PgStat_TableStatus *tablestatus = NULL;
+	RelFileLocator locator;
+
+	if (!pgstat_reloid_to_relfilelocator(rel_id, &locator))
+		return NULL;
+
+	entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION,
+										   locator.dbOid,
+										   RelFileLocatorToPgStatObjid(locator));
 
-	entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, MyDatabaseId, rel_id);
 	if (!entry_ref)
-	{
-		entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, InvalidOid, rel_id);
-		if (!entry_ref)
-			return tablestatus;
-	}
+		return tablestatus;
 
 	tabentry = (PgStat_TableStatus *) entry_ref->pending;
 	tablestatus = palloc_object(PgStat_TableStatus);
@@ -706,8 +777,12 @@ AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
 		record.inserted_pre_truncdrop = trans->inserted_pre_truncdrop;
 		record.updated_pre_truncdrop = trans->updated_pre_truncdrop;
 		record.deleted_pre_truncdrop = trans->deleted_pre_truncdrop;
-		record.id = tabstat->id;
-		record.shared = tabstat->shared;
+
+		if (tabstat->relation != NULL)
+			record.locator = tabstat->relation->rd_locator;
+		else
+			record.locator = tabstat->locator;
+
 		record.truncdropped = trans->truncdropped;
 
 		RegisterTwoPhaseRecord(TWOPHASE_RM_PGSTAT_ID, 0,
@@ -750,7 +825,7 @@ pgstat_twophase_postcommit(FullTransactionId fxid, uint16 info,
 	PgStat_TableStatus *pgstat_info;
 
 	/* Find or create a tabstat entry for the rel */
-	pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared);
+	pgstat_info = pgstat_prep_relation_pending(rec->locator);
 
 	/* Same math as in AtEOXact_PgStat, commit case */
 	pgstat_info->counts.tuples_inserted += rec->tuples_inserted;
@@ -785,8 +860,8 @@ pgstat_twophase_postabort(FullTransactionId fxid, uint16 info,
 	TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata;
 	PgStat_TableStatus *pgstat_info;
 
-	/* Find or create a tabstat entry for the rel */
-	pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared);
+	/* Find or create a tabstat entry for the target locator */
+	pgstat_info = pgstat_prep_relation_pending(rec->locator);
 
 	/* Same math as in AtEOXact_PgStat, abort case */
 	if (rec->truncdropped)
@@ -920,17 +995,21 @@ pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts)
  * initialized if not exists.
  */
 static PgStat_TableStatus *
-pgstat_prep_relation_pending(Oid rel_id, bool isshared)
+pgstat_prep_relation_pending(RelFileLocator locator)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStat_TableStatus *pending;
+	uint64		objid;
+
+	objid = RelFileLocatorToPgStatObjid(locator);
 
 	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_RELATION,
-										  isshared ? InvalidOid : MyDatabaseId,
-										  rel_id, NULL);
+										  locator.dbOid,
+										  objid, NULL);
+
 	pending = entry_ref->pending;
-	pending->id = rel_id;
-	pending->shared = isshared;
+	pending->id = objid;
+	pending->locator = locator;
 
 	return pending;
 }
@@ -1009,3 +1088,82 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
 		trans->tuples_deleted = trans->deleted_pre_truncdrop;
 	}
 }
+
+/*
+ * Convert a relation OID to its corresponding RelFileLocator for statistics
+ * tracking purposes.
+ *
+ * Returns true on success, false if the relation doesn't need statistics
+ * tracking.
+ *
+ * For partitioned tables, constructs a synthetic locator using the relation
+ * OID as relNumber, since they don't have storage.
+ */
+bool
+pgstat_reloid_to_relfilelocator(Oid reloid, RelFileLocator *locator)
+{
+	HeapTuple	tuple;
+	Form_pg_class relform;
+	bool		result = true;
+
+	/* get the relation's tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	relform = (Form_pg_class) GETSTRUCT(tuple);
+
+	/* skip relations without storage and non partitioned tables */
+	if (!RELKIND_HAS_STORAGE(relform->relkind) &&
+		relform->relkind != RELKIND_PARTITIONED_TABLE)
+	{
+		ReleaseSysCache(tuple);
+		return false;
+	}
+
+	if (relform->relkind != RELKIND_PARTITIONED_TABLE)
+	{
+		/* build the RelFileLocator */
+		locator->relNumber = relform->relfilenode;
+		locator->spcOid = relform->reltablespace;
+
+		/* handle default tablespace */
+		if (!OidIsValid(locator->spcOid))
+			locator->spcOid = MyDatabaseTableSpace;
+
+		/* handle dbOid for global vs local relations */
+		if (locator->spcOid == GLOBALTABLESPACE_OID)
+			locator->dbOid = InvalidOid;
+		else
+			locator->dbOid = MyDatabaseId;
+
+		/* handle mapped relations */
+		if (!RelFileNumberIsValid(locator->relNumber))
+		{
+			locator->relNumber = RelationMapOidToFilenumber(reloid,
+															relform->relisshared);
+			if (!RelFileNumberIsValid(locator->relNumber))
+			{
+				ReleaseSysCache(tuple);
+				return false;
+			}
+		}
+	}
+	else
+	{
+		/*
+		 * Partitioned tables don't have storage, so construct a synthetic
+		 * locator for statistics tracking. Use a reserved pseudo tablespace
+		 * OID that cannot conflict with real tablespaces, and the relation
+		 * OID as relNumber. This ensures no collision with regular relations
+		 * even after OID wraparound.
+		 */
+		locator->dbOid = (relform->relisshared ? InvalidOid : MyDatabaseId);
+		locator->spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+		locator->relNumber = relform->oid;
+	}
+
+	ReleaseSysCache(tuple);
+	return result;
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ef6fffe60b9..60ffb1679ec 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -23,13 +23,13 @@
 #include "common/ip.h"
 #include "funcapi.h"
 #include "miscadmin.h"
-#include "pgstat.h"
 #include "postmaster/bgworker.h"
 #include "replication/logicallauncher.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
 #include "utils/timestamp.h"
 
 #define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))
@@ -1949,9 +1949,14 @@ Datum
 pg_stat_reset_single_table_counters(PG_FUNCTION_ARGS)
 {
 	Oid			taboid = PG_GETARG_OID(0);
-	Oid			dboid = (IsSharedRelation(taboid) ? InvalidOid : MyDatabaseId);
+	RelFileLocator locator;
 
-	pgstat_reset(PGSTAT_KIND_RELATION, dboid, taboid);
+	/* Get the stats locator from the relation OID */
+	if (!pgstat_reloid_to_relfilelocator(taboid, &locator))
+		PG_RETURN_VOID();
+
+	pgstat_reset(PGSTAT_KIND_RELATION, locator.dbOid,
+				 RelFileLocatorToPgStatObjid(locator));
 
 	PG_RETURN_VOID();
 }
@@ -2305,5 +2310,16 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 	uint64		objid = PG_GETARG_INT64(2);
 	PgStat_Kind kind = pgstat_get_kind_from_str(stats_type);
 
+	/* Convert relation OID to relfilenode objid */
+	if (kind == PGSTAT_KIND_RELATION)
+	{
+		RelFileLocator locator;
+
+		if (!pgstat_reloid_to_relfilelocator(objid, &locator))
+			PG_RETURN_BOOL(false);
+
+		objid = RelFileLocatorToPgStatObjid(locator);
+	}
+
 	PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
 }
diff --git a/src/include/catalog/pg_tablespace.dat b/src/include/catalog/pg_tablespace.dat
index 1302a3d75cd..9430970fffd 100644
--- a/src/include/catalog/pg_tablespace.dat
+++ b/src/include/catalog/pg_tablespace.dat
@@ -10,6 +10,10 @@
 #
 #----------------------------------------------------------------------
 
+/*
+ * When adding a new one, ensure it does not conflict with
+ * PSEUDO_PARTITION_TABLE_SPCOID.
+ */
 [
 
 { oid => '1663', oid_symbol => 'DEFAULTTABLESPACE_OID',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 7816d779d8c..0e2d8051d69 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -21,6 +21,14 @@
 #include "catalog/genbki.h"
 #include "catalog/pg_tablespace_d.h"	/* IWYU pragma: export */
 
+/*
+ * Reserved tablespace OID for partitioned table pseudo locators.
+ * This is not an actual tablespace, just a reserved value to distinguish
+ * partitioned table statistics from regular table statistics. Ensures it does
+ * not conflict with the ones in pg_tablespace.dat.
+ */
+#define PSEUDO_PARTITION_TABLE_SPCOID 1665
+
 /* ----------------
  *		pg_tablespace definition.  cpp turns this into
  *		typedef struct FormData_pg_tablespace
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 6714363144a..3102f86aa24 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
 #include "postmaster/pgarch.h"	/* for MAX_XFN_CHARS */
 #include "replication/conflict.h"
 #include "replication/worker_internal.h"
+#include "storage/relfilelocator.h"
 #include "utils/backend_progress.h" /* for backward compatibility */	/* IWYU pragma: export */
 #include "utils/backend_status.h"	/* for backward compatibility */	/* IWYU pragma: export */
 #include "utils/pgstat_kind.h"
@@ -35,6 +36,12 @@
 /* Default directory to store temporary statistics data in */
 #define PG_STAT_TMP_DIR		"pg_stat_tmp"
 
+/*
+ * Build a pgstat key Objid based on a RelFileLocator.
+ */
+#define RelFileLocatorToPgStatObjid(locator) \
+	(((uint64) (locator).spcOid << 32) | (locator).relNumber)
+
 /* Values for track_functions GUC variable --- order is significant! */
 typedef enum TrackFunctionsLevel
 {
@@ -175,11 +182,11 @@ typedef struct PgStat_TableCounts
  */
 typedef struct PgStat_TableStatus
 {
-	Oid			id;				/* table's OID */
-	bool		shared;			/* is it a shared catalog? */
+	uint64		id;				/* hash of relfilelocator for stats key */
 	struct PgStat_TableXactStatus *trans;	/* lowest subxact's counts */
 	PgStat_TableCounts counts;	/* event counts to be sent */
 	Relation	relation;		/* rel that is using this entry */
+	RelFileLocator locator;		/* table's relfilelocator */
 } PgStat_TableStatus;
 
 /* ----------
@@ -735,8 +742,8 @@ extern void pgstat_twophase_postabort(FullTransactionId fxid, uint16 info,
 									  void *recdata, uint32 len);
 
 extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry(Oid relid);
-extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(bool shared,
-														   Oid reloid);
+extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_by_locator(RelFileLocator locator);
+extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(Oid reloid);
 extern PgStat_TableStatus *find_tabstat_entry(Oid rel_id);
 
 
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 5c1ce4d3d6a..7b24928b00d 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -764,6 +764,7 @@ extern void PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state);
 extern bool pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
 extern void pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref);
 extern void pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts);
+extern bool pgstat_reloid_to_relfilelocator(Oid reloid, RelFileLocator *locator);
 
 
 /*
diff --git a/src/test/recovery/t/029_stats_restart.pl b/src/test/recovery/t/029_stats_restart.pl
index 021e2bf361f..3a9c05eaf10 100644
--- a/src/test/recovery/t/029_stats_restart.pl
+++ b/src/test/recovery/t/029_stats_restart.pl
@@ -55,10 +55,10 @@ trigger_funcrel_stat();
 
 # verify stats objects exist
 $sect = "initial";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	't', "$sect: relation stats do exist");
 
 # regular shutdown
@@ -79,10 +79,10 @@ copy($og_stats, $statsfile) or die "Copy failed: $!";
 $node->start;
 
 $sect = "copy";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	't', "$sect: relation stats do exist");
 
 $node->stop('immediate');
@@ -96,10 +96,10 @@ $node->start;
 
 # stats should have been discarded
 $sect = "post immediate";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	'f', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	'f', "$sect: relation stats do not exist");
 
 # get rid of backup statsfile
@@ -110,10 +110,10 @@ unlink $statsfile or die "cannot unlink $statsfile $!";
 trigger_funcrel_stat();
 
 $sect = "post immediate, new";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	't', "$sect: relation stats do exist");
 
 # regular shutdown
@@ -129,10 +129,10 @@ $node->start;
 
 # no stats present due to invalid stats file
 $sect = "invalid_overwrite";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	'f', "$sect: relation stats do not exist");
 
 
@@ -145,10 +145,10 @@ append_file($og_stats, "XYZ");
 $node->start;
 
 $sect = "invalid_append";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
 	'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
 	'f', "$sect: relation stats do not exist");
 
 
@@ -307,9 +307,9 @@ sub trigger_funcrel_stat
 
 sub have_stats
 {
-	my ($kind, $dboid, $objid) = @_;
+	my ($db, $kind, $dboid, $objid) = @_;
 
-	return $node->safe_psql($connect_db,
+	return $node->safe_psql($db,
 		"SELECT pg_stat_have_stats('$kind', $dboid, $objid)");
 }
 
-- 
2.34.1


--xJsrX25x3tm7Tn81
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v10-0002-handle-relation-statistics-correctly-during-rewr.patch"



^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Ewan Young <[email protected]>
Discussion: https://postgr.es/m/akS/ldidWeqG1FWk%40bdtpg
---
 src/backend/utils/activity/pgstat.c       | 46 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 7 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..540db1ef115 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,14 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
-	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1338,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1361,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--cdkC2WBRrpXQO1hi--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 05:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 05:43 UTC (permalink / raw)

If a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call
to pgstat_register_kind(). The SQL functions are still created, and
calling them invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would
dereference NULL and segfault.

Add runtime checks in all public-facing pgstat functions that accept a
PgStat_Kind and dereference the returned kind info:

  - pgstat_prep_pending_entry()
  - pgstat_fetch_entry()
  - pgstat_reset()
  - pgstat_reset_of_kind()
  - pgstat_have_entry()
  - pgstat_snapshot_fixed()
  - pgstat_init_entry()
  - pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

This affects any extension using the custom cumulative statistics API
introduced in PG18.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 45 ++++++++++++++++++++---
 src/backend/utils/activity/pgstat_shmem.c | 13 ++++++-
 2 files changed, 52 insertions(+), 6 deletions(-)
 100.0% src/backend/utils/activity/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index c4fa14f138f..5180201c4e8 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -885,8 +885,13 @@ pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* not needed atm, and doesn't make sense with the current signature */
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	/* reset the "single counter" */
 	pgstat_reset_entry(kind, dboid, objid, ts);
@@ -907,6 +912,11 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 	TimestampTz ts = GetCurrentTimestamp();
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	if (kind_info->fixed_amount)
 		kind_info->reset_all_cb(ts);
 	else
@@ -967,6 +977,11 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free)
 	void	   *stats_data;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* should be called from backends */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 	Assert(!kind_info->fixed_amount);
@@ -1088,8 +1103,15 @@ pgstat_get_stat_snapshot_timestamp(bool *have_snapshot)
 bool
 pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/* fixed-numbered stats always exist */
-	if (pgstat_get_kind_info(kind)->fixed_amount)
+	if (kind_info->fixed_amount)
 		return true;
 
 	return pgstat_get_entry_ref(kind, dboid, objid, false, NULL) != NULL;
@@ -1104,8 +1126,15 @@ pgstat_have_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 void
 pgstat_snapshot_fixed(PgStat_Kind kind)
 {
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	Assert(pgstat_is_kind_valid(kind));
-	Assert(pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(kind_info->fixed_amount);
 
 	if (force_stats_snapshot_clear)
 		pgstat_clear_snapshot();
@@ -1310,9 +1339,15 @@ PgStat_EntryRef *
 pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *created_entry)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
 	/* need to be able to flush out */
-	Assert(pgstat_get_kind_info(kind)->flush_pending_cb != NULL);
+	Assert(kind_info->flush_pending_cb != NULL);
 
 	if (unlikely(!pgStatPendingContext))
 	{
@@ -1327,7 +1362,7 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 	if (entry_ref->pending == NULL)
 	{
-		size_t		entrysize = pgstat_get_kind_info(kind)->pending_size;
+		size_t		entrysize = kind_info->pending_size;
 
 		Assert(entrysize != (size_t) -1);
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 5ea3f1973f9..4e6a556af93 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -318,6 +318,11 @@ pgstat_init_entry(PgStat_Kind kind,
 	PgStatShared_Common *shheader;
 	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
 
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
+
 	/*
 	 * Initialize refcount to 1, marking it as valid / not dropped. The entry
 	 * can't be freed before the initialization because it can't be found as
@@ -1127,8 +1132,14 @@ void
 pgstat_reset_entry(PgStat_Kind kind, Oid dboid, uint64 objid, TimestampTz ts)
 {
 	PgStat_EntryRef *entry_ref;
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+	if (unlikely(kind_info == NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("statistics kind %u is not known or registered", kind)));
 
-	Assert(!pgstat_get_kind_info(kind)->fixed_amount);
+	Assert(!kind_info->fixed_amount);
 
 	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
 	if (!entry_ref || entry_ref->shared_entry->dropped)
-- 
2.34.1


--jlSGGOUQ0kGWBHE+--





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 07:19  Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-01 07:19 UTC (permalink / raw)
  To: [email protected]; +Cc: Michael Paquier <[email protected]>

Hi hackers,

While reviewing [1], I got segfault(s) because I created a custom statistics
extension that I forgot to add to shared_preload_libraries. Then using one of
its function produced:

"
Core was generated by `postgres: postgres postgres [local] SELECT                                    '.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  pgstat_init_entry (kind=kind@entry=24, shhashent=shhashent@entry=0x73f6c341a740) at pgstat_shmem.c:335
335             chunk = dsa_allocate_extended(pgStatLocal.dsa,
"

Indeed, if a custom statistics extension is loaded via CREATE EXTENSION without
being listed in shared_preload_libraries, its _PG_init() skips the call to
pgstat_register_kind(). The SQL functions are still created, and calling them
invokes pgstat functions with a kind that was never registered.

pgstat_get_kind_info() returns NULL in this case. The existing code only
checked this via Assert() in some paths, so non-assert builds would dereference
NULL and segfault.

The attached patch adds runtime checks in all public-facing pgstat functions that
accept a PgStat_Kind and dereference the returned kind info:

- pgstat_prep_pending_entry()
- pgstat_fetch_entry()
- pgstat_reset()
- pgstat_reset_of_kind()
- pgstat_have_entry()
- pgstat_snapshot_fixed()
- pgstat_init_entry()
- pgstat_reset_entry()

Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
the kind is not known or registered.

[1]: https://postgr.es/m/akSi2txzLZWQL31Q%40bdtpg

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com


^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-01 08:20  Ewan Young <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Ewan Young @ 2026-07-01 08:20 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: [email protected]; Michael Paquier <[email protected]>

Hi Bertrand,

On Wed, Jul 1, 2026 at 3:20 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi hackers,
>
> While reviewing [1], I got segfault(s) because I created a custom statistics
> extension that I forgot to add to shared_preload_libraries. Then using one of
> its function produced:
>
> "
> Core was generated by `postgres: postgres postgres [local] SELECT                                    '.
> Program terminated with signal SIGSEGV, Segmentation fault.
> #0  pgstat_init_entry (kind=kind@entry=24, shhashent=shhashent@entry=0x73f6c341a740) at pgstat_shmem.c:335
> 335             chunk = dsa_allocate_extended(pgStatLocal.dsa,
> "
>
> Indeed, if a custom statistics extension is loaded via CREATE EXTENSION without
> being listed in shared_preload_libraries, its _PG_init() skips the call to
> pgstat_register_kind(). The SQL functions are still created, and calling them
> invokes pgstat functions with a kind that was never registered.
>
> pgstat_get_kind_info() returns NULL in this case. The existing code only
> checked this via Assert() in some paths, so non-assert builds would dereference
> NULL and segfault.
>
> The attached patch adds runtime checks in all public-facing pgstat functions that
> accept a PgStat_Kind and dereference the returned kind info:
>
> - pgstat_prep_pending_entry()
> - pgstat_fetch_entry()
> - pgstat_reset()
> - pgstat_reset_of_kind()
> - pgstat_have_entry()
> - pgstat_snapshot_fixed()
> - pgstat_init_entry()
> - pgstat_reset_entry()
>
> Each now raises ERROR with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE when
> the kind is not known or registered.

Thanks for the patch — nice catch, and the diagnosis looks right.

One small thing: in pgstat_snapshot_fixed(), the existing
Assert(pgstat_is_kind_valid(kind)); becomes redundant after the new NULL
check. A non-NULL kind_info already implies the kind is valid (that's the
only way pgstat_get_kind_info() returns non-NULL), so the assert can never
fire. Might as well drop it and keep just the fixed_amount one.

>
> [1]: https://postgr.es/m/akSi2txzLZWQL31Q%40bdtpg
>
> Regards,
>
> --
> Bertrand Drouvot
> PostgreSQL Contributors Team
> RDS Open Source Databases
> Amazon Web Services: https://aws.amazon.com

-- 
Regards,
Ewan Young





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 03:27  Bertrand Drouvot <[email protected]>
  parent: Ewan Young <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-02 03:27 UTC (permalink / raw)
  To: Ewan Young <[email protected]>; +Cc: [email protected]; Michael Paquier <[email protected]>

Hi Ewan,

On Wed, Jul 01, 2026 at 04:20:39PM +0800, Ewan Young wrote:
> Thanks for the patch — nice catch, and the diagnosis looks right.

Thanks for looking at it!

> One small thing: in pgstat_snapshot_fixed(), the existing
> Assert(pgstat_is_kind_valid(kind)); becomes redundant after the new NULL
> check. A non-NULL kind_info already implies the kind is valid (that's the
> only way pgstat_get_kind_info() returns non-NULL), so the assert can never
> fire. Might as well drop it and keep just the fixed_amount one.

Yeah good catch, done in the attached.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com


^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 03:43  Michael Paquier <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Michael Paquier @ 2026-07-02 03:43 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

On Thu, Jul 02, 2026 at 03:27:18AM +0000, Bertrand Drouvot wrote:
> On Wed, Jul 01, 2026 at 04:20:39PM +0800, Ewan Young wrote:
>> One small thing: in pgstat_snapshot_fixed(), the existing
>> Assert(pgstat_is_kind_valid(kind)); becomes redundant after the new NULL
>> check. A non-NULL kind_info already implies the kind is valid (that's the
>> only way pgstat_get_kind_info() returns non-NULL), so the assert can never
>> fire. Might as well drop it and keep just the fixed_amount one.
> 
> Yeah good catch, done in the attached.

I am not convinced that it is worth bothering in the core code about
this class of failures; they are just not interesting, and impossible
to miss.

It seems to me that this error is in the _PG_init() of the modules in
modules/test_custom_stats/: we should not bypass the
pgstat_register_kind() if not loading the library from
shared_preload_libraries, but let the call happen and fail.
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 04:06  Bertrand Drouvot <[email protected]>
  parent: Michael Paquier <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-02 04:06 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

Hi,

On Thu, Jul 02, 2026 at 12:43:43PM +0900, Michael Paquier wrote:
> On Thu, Jul 02, 2026 at 03:27:18AM +0000, Bertrand Drouvot wrote:
> > On Wed, Jul 01, 2026 at 04:20:39PM +0800, Ewan Young wrote:
> >> One small thing: in pgstat_snapshot_fixed(), the existing
> >> Assert(pgstat_is_kind_valid(kind)); becomes redundant after the new NULL
> >> check. A non-NULL kind_info already implies the kind is valid (that's the
> >> only way pgstat_get_kind_info() returns non-NULL), so the assert can never
> >> fire. Might as well drop it and keep just the fixed_amount one.
> > 
> > Yeah good catch, done in the attached.
> 
> I am not convinced that it is worth bothering in the core code about
> this class of failures; they are just not interesting, and impossible
> to miss.
> 
> It seems to me that this error is in the _PG_init() of the modules in
> modules/test_custom_stats/: we should not bypass the
> pgstat_register_kind() if not loading the library from
> shared_preload_libraries, but let the call happen and fail.

I agree that the responsibility should primarily be in the extension. However,
the issue is that the NULL dereference happens inside core code (pgstat_prep_pending_entry,
etc.), and the resulting segfault(s) cause the postmaster to terminate all
backends (not just the offending session).

Given that one misconfigured extension can crash all connections on the server,
a defensive check in core seems reasonable (kind of similar to 341e9a05e7b).

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 04:10  Michael Paquier <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Michael Paquier @ 2026-07-02 04:10 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

On Thu, Jul 02, 2026 at 04:06:01AM +0000, Bertrand Drouvot wrote:
> I agree that the responsibility should primarily be in the extension. However,
> the issue is that the NULL dereference happens inside core code (pgstat_prep_pending_entry,
> etc.), and the resulting segfault(s) cause the postmaster to terminate all
> backends (not just the offending session).
> 
> Given that one misconfigured extension can crash all connections on the server,
> a defensive check in core seems reasonable (kind of similar to 341e9a05e7b).

Nope, this was a different thing, doable in a couple of steps:
- Load the library.
- Write custom stats.
- Stop the server, flush the stats.
- Edit the configuration, not loading the library.
- Restart the server, loading failed.

The problem of this thread ought to be blocked at its source, in the
extension itself: let's not give free hands to an extension to do what
it should not be allowed to do.  There is a similar defense in
test_custom_rmgrs, as one example.  We should just map to that.
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 04:23  Bertrand Drouvot <[email protected]>
  parent: Michael Paquier <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-02 04:23 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

Hi,

On Thu, Jul 02, 2026 at 01:10:18PM +0900, Michael Paquier wrote:
> On Thu, Jul 02, 2026 at 04:06:01AM +0000, Bertrand Drouvot wrote:
> > I agree that the responsibility should primarily be in the extension. However,
> > the issue is that the NULL dereference happens inside core code (pgstat_prep_pending_entry,
> > etc.), and the resulting segfault(s) cause the postmaster to terminate all
> > backends (not just the offending session).
> > 
> > Given that one misconfigured extension can crash all connections on the server,
> > a defensive check in core seems reasonable (kind of similar to 341e9a05e7b).
> 
> Nope, this was a different thing, doable in a couple of steps:
> - Load the library.
> - Write custom stats.
> - Stop the server, flush the stats.
> - Edit the configuration, not loading the library.
> - Restart the server, loading failed.
> 
> The problem of this thread ought to be blocked at its source, in the
> extension itself: let's not give free hands to an extension to do what
> it should not be allowed to do.  There is a similar defense in
> test_custom_rmgrs, as one example.  We should just map to that.

Ok but what about extensions that don't call pgstat_register_kind() at all? Your
point is that they would see the issue during the development of the extension? (If
so, I think I could agree).

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 04:43  Bertrand Drouvot <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-02 04:43 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

On Thu, Jul 02, 2026 at 04:23:16AM +0000, Bertrand Drouvot wrote:
> Hi,
> 
> On Thu, Jul 02, 2026 at 01:10:18PM +0900, Michael Paquier wrote:
> > On Thu, Jul 02, 2026 at 04:06:01AM +0000, Bertrand Drouvot wrote:
> > > I agree that the responsibility should primarily be in the extension. However,
> > > the issue is that the NULL dereference happens inside core code (pgstat_prep_pending_entry,
> > > etc.), and the resulting segfault(s) cause the postmaster to terminate all
> > > backends (not just the offending session).
> > > 
> > > Given that one misconfigured extension can crash all connections on the server,
> > > a defensive check in core seems reasonable (kind of similar to 341e9a05e7b).
> > 
> > Nope, this was a different thing, doable in a couple of steps:
> > - Load the library.
> > - Write custom stats.
> > - Stop the server, flush the stats.
> > - Edit the configuration, not loading the library.
> > - Restart the server, loading failed.
> > 
> > The problem of this thread ought to be blocked at its source, in the
> > extension itself: let's not give free hands to an extension to do what
> > it should not be allowed to do.  There is a similar defense in
> > test_custom_rmgrs, as one example.  We should just map to that.
> 
> Ok but what about extensions that don't call pgstat_register_kind() at all? Your
> point is that they would see the issue during the development of the extension? (If
> so, I think I could agree).

Something like in the attached?

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com


^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 05:00  Michael Paquier <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Michael Paquier @ 2026-07-02 05:00 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

On Thu, Jul 02, 2026 at 04:43:32AM +0000, Bertrand Drouvot wrote:
> On Thu, Jul 02, 2026 at 04:23:16AM +0000, Bertrand Drouvot wrote:
>> Ok but what about extensions that don't call pgstat_register_kind() at all? Your
>> point is that they would see the issue during the development of the extension? (If
>> so, I think I could agree).

Extensions doing custom stats have to call the register API, or
they're broken.  This is the same assumption as custom RMGRs.  There
are many ways to break the backend if you don't know what you do, just
take hooks for example.  That's just one of them.

> Something like in the attached?

Yes, that looks OK here.
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 05:02  Bertrand Drouvot <[email protected]>
  parent: Michael Paquier <[email protected]>
  0 siblings, 1 reply; 452+ messages in thread

From: Bertrand Drouvot @ 2026-07-02 05:02 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

Hi,

On Thu, Jul 02, 2026 at 02:00:06PM +0900, Michael Paquier wrote:
> On Thu, Jul 02, 2026 at 04:43:32AM +0000, Bertrand Drouvot wrote:
> > On Thu, Jul 02, 2026 at 04:23:16AM +0000, Bertrand Drouvot wrote:
> >> Ok but what about extensions that don't call pgstat_register_kind() at all? Your
> >> point is that they would see the issue during the development of the extension? (If
> >> so, I think I could agree).
> 
> Extensions doing custom stats have to call the register API, or
> they're broken.  This is the same assumption as custom RMGRs.  There
> are many ways to break the backend if you don't know what you do, just
> take hooks for example.  That's just one of them.
> 
> > Something like in the attached?
> 
> Yes, that looks OK here.

That makes sense, I do agree.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 452+ messages in thread

* Re: Prevent crash when calling pgstat functions with unregistered stats kind
@ 2026-07-02 06:53  Michael Paquier <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 452+ messages in thread

From: Michael Paquier @ 2026-07-02 06:53 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Ewan Young <[email protected]>; [email protected]

On Thu, Jul 02, 2026 at 05:02:36AM +0000, Bertrand Drouvot wrote:
> That makes sense, I do agree.

Done that now down to v19, thanks, in the shape of some pure code
deletion.
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 452+ messages in thread


end of thread, other threads:[~2026-07-02 06:53 UTC | newest]

Thread overview: 452+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-10-01 09:45 [PATCH v10 1/2] Key PGSTAT_KIND_RELATION by relfile locator Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v1] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 05:43 [PATCH v2] Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 07:19 Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-01 08:20 ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Ewan Young <[email protected]>
2026-07-02 03:27   ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-02 03:43     ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Michael Paquier <[email protected]>
2026-07-02 04:06       ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-02 04:10         ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Michael Paquier <[email protected]>
2026-07-02 04:23           ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-02 04:43             ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-02 05:00               ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Michael Paquier <[email protected]>
2026-07-02 05:02                 ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Bertrand Drouvot <[email protected]>
2026-07-02 06:53                   ` Re: Prevent crash when calling pgstat functions with unregistered stats kind Michael Paquier <[email protected]>

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