From 2d0a40637d6c463346e7cce318419909366b1847 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Mon, 13 Jul 2026 14:25:25 -0500
Subject: [PATCH v1 2/2] pgstat: change to_serialized_data callback to return
 bool

Change the to_serialized_data callback signature from void to bool,
making it symmetric with from_serialized_data.  This allows extensions
to signal write failure back to pgstat_write_statsfile().

When the callback returns false, pgstat_write_statsfile() discards the
temporary stats file, preventing a corrupt file from being committed
to disk.

Update test_custom_stats to check write_chunk() return values and
propagate failure through the new bool return.
---
 src/backend/utils/activity/pgstat.c           | 16 ++++++++--
 src/include/utils/pgstat_internal.h           |  6 ++--
 .../test_custom_stats/test_custom_var_stats.c | 29 ++++++++++++-------
 3 files changed, 36 insertions(+), 15 deletions(-)

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index debcc87af50..77bd3880b59 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -1775,8 +1775,12 @@ pgstat_write_statsfile(void)
 					pgstat_get_entry_len(ps->key.kind));
 
 		/* Write more data for the entry, if required */
-		if (kind_info->to_serialized_data)
-			kind_info->to_serialized_data(&ps->key, shstats, fpout);
+		if (kind_info->to_serialized_data &&
+			!kind_info->to_serialized_data(&ps->key, shstats, fpout))
+		{
+			dshash_seq_term(&hstat);
+			goto error;
+		}
 	}
 	dshash_seq_term(&hstat);
 
@@ -1810,6 +1814,14 @@ pgstat_write_statsfile(void)
 		unlink(tmpfile);
 	}
 
+	goto finish;
+
+error:
+	elog(DEBUG2, "discarding temporary statistics file \"%s\"", tmpfile);
+	FreeFile(fpout);
+	unlink(tmpfile);
+
+finish:
 	/* Finish callbacks, if required */
 	for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
 	{
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 02022f19b81..1c03e797cbb 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -322,7 +322,9 @@ typedef struct PgStat_KindInfo
 	 * an entry, in the stats file or optionally in a different file.
 	 * Optional.
 	 *
-	 * to_serialized_data: write auxiliary data for an entry.
+	 * to_serialized_data: write auxiliary data for an entry.  Returns true on
+	 * success, false on write error.  When false is returned,
+	 * pgstat_write_statsfile() will discard the stats file.
 	 *
 	 * from_serialized_data: read auxiliary data for an entry.  Returns true
 	 * on success, false on read error.
@@ -332,7 +334,7 @@ typedef struct PgStat_KindInfo
 	 * just written or read.  "header" is a pointer to the stats data; it may
 	 * be modified only in from_serialized_data to reconstruct an entry.
 	 */
-	void		(*to_serialized_data) (const PgStat_HashKey *key,
+	bool		(*to_serialized_data) (const PgStat_HashKey *key,
 									   const PgStatShared_Common *header,
 									   FILE *statfile);
 	bool		(*from_serialized_data) (const PgStat_HashKey *key,
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c
index 34d474be604..a39ada0b67c 100644
--- a/src/test/modules/test_custom_stats/test_custom_var_stats.c
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c
@@ -26,7 +26,7 @@ PG_MODULE_MAGIC_EXT(
 );
 
 /* Local helpers for stats file I/O */
-#define write_chunk(fpout, ptr, len) ((void) fwrite(ptr, len, 1, fpout))
+#define write_chunk(fpout, ptr, len) (fwrite(ptr, len, 1, fpout) == 1)
 #define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr))
 #define read_chunk(fpin, ptr, len) (fread(ptr, 1, len, fpin) == (len))
 #define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr))
@@ -94,7 +94,7 @@ static bool test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref,
 												   bool nowait);
 
 /* Serialization callback: write auxiliary entry data */
-static void test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
+static bool test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 													 const PgStatShared_Common *header,
 													 FILE *statfile);
 
@@ -193,7 +193,7 @@ test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait)
  * - The length of the description.
  * - The description data itself.
  */
-static void
+static bool
 test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 										 const PgStatShared_Common *header,
 										 FILE *statfile)
@@ -208,7 +208,8 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	 * First mark the main file with a magic number, keeping a trace that some
 	 * auxiliary data will exist in the secondary statistics file.
 	 */
-	write_chunk_s(statfile, &magic_number);
+	if (!write_chunk_s(statfile, &magic_number))
+		return false;
 
 	/* Open statistics file for writing. */
 	if (!fd_description)
@@ -220,7 +221,7 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 					(errcode_for_file_access(),
 					 errmsg("could not open statistics file \"%s\" for writing: %m",
 							TEST_CUSTOM_AUX_DATA_DESC)));
-			return;
+			return false;
 		}
 
 		/* Initialize offset for secondary statistics file. */
@@ -228,14 +229,16 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	}
 
 	/* Write offset to the main data file */
-	write_chunk_s(statfile, &fd_description_offset);
+	if (!write_chunk_s(statfile, &fd_description_offset))
+		return false;
 
 	/*
 	 * First write the entry key to the secondary statistics file.  This will
 	 * be cross-checked with the key read from main stats file at loading
 	 * time.
 	 */
-	write_chunk_s(fd_description, (PgStat_HashKey *) key);
+	if (!write_chunk_s(fd_description, (PgStat_HashKey *) key))
+		return false;
 	fd_description_offset += sizeof(PgStat_HashKey);
 
 	if (!custom_stats_description_dsa)
@@ -246,9 +249,10 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	{
 		/* length to description file */
 		len = 0;
-		write_chunk_s(fd_description, &len);
+		if (!write_chunk_s(fd_description, &len))
+			return false;
 		fd_description_offset += sizeof(size_t);
-		return;
+		return true;
 	}
 
 	/*
@@ -258,14 +262,17 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	description = dsa_get_address(custom_stats_description_dsa,
 								  entry->description);
 	len = strlen(description) + 1;
-	write_chunk_s(fd_description, &len);
-	write_chunk(fd_description, description, len);
+	if (!write_chunk_s(fd_description, &len))
+		return false;
+	if (!write_chunk(fd_description, description, len))
+		return false;
 
 	/*
 	 * Update offset for next entry, counting for the length (size_t) of the
 	 * description and the description contents.
 	 */
 	fd_description_offset += len + sizeof(size_t);
+	return true;
 }
 
 /*
-- 
2.50.1 (Apple Git-155)

