On 06/11/2026 06:26, Michael Paquier wrote:
> Note that for pg_visibility, read_stream has some protections when it
> comes to temp tables of other sessions, but that does not fly under
> !include_pd.  Merged 0004 and 0005 together, tweaked a bit the whole,
> and applied.
> 
> pgrowlocks (uses read_stream, safe for temp from other settions,
> complains about recovery) has a single point of failure, with a weird
> error message.  Done as well.


Thanks!


> Regarding pg_freespacemap, I am not convinced.  Returning 0 reflects
> also the reality: there is no FSM for the temp-from-other-session case
> and temp/unlogged at recovery.


Fair enough.


> For pageinspect, we have a pageinspect.h, where I'd suggest to have a
> common routine for the whole to remove the duplication.  For
> pgstattuple, we don't have a "common" path, but that seems worth
> having to avoid duplicating this kind of check all over the place.

PFA v5.

0001 (pageinspect) and 0002 (pgstattuple) add the helper function
check_relation_accessible(), where the tests for unlogged and temporary
tables now live. While at it, I moved the existing
RELATION_IS_OTHER_TEMP(rel) check to the same function, so that all
checks are kept in one place.

Best, Jim
From 95f3471c8d97bbfb17314035b9583eba6bbb2b9e Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Fri, 6 Nov 2026 13:59:56 +0100
Subject: [PATCH v5 1/2] pageinspect: Reject unlogged relations during recovery

Unlogged tables and indexes have no data on a standby, so reading one
there must fail.  get_raw_page(), bt_metap(), bt_page_stats(),
bt_multi_page_stats(), bt_page_items() and hash_bitmap_info() read the
relation directly instead of going through the planner, so on a standby
they failed with a confusing low-level message instead of the error
that SELECT reports:

  ERROR:  could not open file "base/5/16384": No such file or directory

Check for this case and report the same error as SELECT:

  ERROR:  cannot access temporary or unlogged relations during recovery

The init fork of an unlogged relation does exist on a standby, so
get_raw_page() can still read it.

This check and the existing check against temporary relations of other
sessions are now grouped in the helper check_relation_accessible(),
shared by all these functions.

heap_page_item_attrs() and tuple_data_split() can also reach unlogged
storage when detoasting values from a saved page image, so check the
persistence of the TOAST relation referenced by each external on-disk
value before detoasting as well.

Author: Jim Jones <[email protected]>
Co-authored-by: Kirill Reshke <[email protected]>
Co-authored-by: Surya Poondla <[email protected]>
Reported-by: Zsolt Parragi <[email protected]>
Reviewed-by: Michael Paquier <[email protected]>
Discussion: https://www.postgresql.org/message-id/flat/[email protected]
---
 contrib/pageinspect/btreefuncs.c  | 20 ++--------------
 contrib/pageinspect/hashfuncs.c   |  5 +---
 contrib/pageinspect/heapfuncs.c   | 25 +++++++++++++++++++
 contrib/pageinspect/pageinspect.h |  3 +++
 contrib/pageinspect/rawpage.c     | 40 ++++++++++++++++++++++++-------
 5 files changed, 62 insertions(+), 31 deletions(-)

diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 9917663593b..c9a6c2cf3fa 100644
--- a/contrib/pageinspect/btreefuncs.c
+++ b/contrib/pageinspect/btreefuncs.c
@@ -231,15 +231,7 @@ bt_index_block_validate(Relation rel, int64 blkno)
 				 errmsg("\"%s\" is not a %s index",
 						RelationGetRelationName(rel), "btree")));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel, MAIN_FORKNUM);
 
 	if (blkno == 0)
 		ereport(ERROR,
@@ -864,15 +856,7 @@ bt_metap(PG_FUNCTION_ARGS)
 				 errmsg("\"%s\" is not a %s index",
 						RelationGetRelationName(rel), "btree")));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel, MAIN_FORKNUM);
 
 	buffer = ReadBuffer(rel, 0);
 	LockBuffer(buffer, BUFFER_LOCK_SHARE);
diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c
index 30870329cc9..340bfa21c15 100644
--- a/contrib/pageinspect/hashfuncs.c
+++ b/contrib/pageinspect/hashfuncs.c
@@ -426,10 +426,7 @@ hash_bitmap_info(PG_FUNCTION_ARGS)
 				 errmsg("\"%s\" is not a %s index",
 						RelationGetRelationName(indexRel), "hash")));
 
-	if (RELATION_IS_OTHER_TEMP(indexRel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(indexRel, MAIN_FORKNUM);
 
 	if (ovflblkno < 0 || ovflblkno > MaxBlockNumber)
 		ereport(ERROR,
diff --git a/contrib/pageinspect/heapfuncs.c b/contrib/pageinspect/heapfuncs.c
index 7be4770dc84..b2a14444dcc 100644
--- a/contrib/pageinspect/heapfuncs.c
+++ b/contrib/pageinspect/heapfuncs.c
@@ -25,8 +25,10 @@
 
 #include "postgres.h"
 
+#include "access/detoast.h"
 #include "access/htup_details.h"
 #include "access/relation.h"
+#include "access/xlog.h"
 #include "catalog/pg_am_d.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
@@ -36,7 +38,9 @@
 #include "port/pg_bitutils.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 
 /*
  * It's not supported to create tuples with oids anymore, but when pg_upgrade
@@ -389,7 +393,28 @@ tuple_data_split_internal(Oid relid, char *tupdata,
 						 errmsg("unexpected end of tuple data")));
 
 			if (attr->attlen == -1 && do_detoast)
+			{
+				/* A saved page can still refer to unlogged TOAST storage. */
+				if (VARATT_IS_EXTERNAL_ONDISK(tupdata + off) &&
+					RecoveryInProgress())
+				{
+					toast_external_data toast_ext_data;
+
+					toast_external_info_get((varlena *) (tupdata + off),
+											&toast_ext_data);
+
+					/* If the relation is gone, let detoasting complain. */
+					if (SearchSysCacheExists1(RELOID,
+											  ObjectIdGetDatum(toast_ext_data.toastrelid)) &&
+						get_rel_persistence(toast_ext_data.toastrelid) !=
+						RELPERSISTENCE_PERMANENT)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("cannot access temporary or unlogged relations during recovery")));
+				}
+
 				attr_data = pg_detoast_datum_copy((varlena *) (tupdata + off));
+			}
 			else
 			{
 				attr_data = (bytea *) palloc(len + VARHDRSZ);
diff --git a/contrib/pageinspect/pageinspect.h b/contrib/pageinspect/pageinspect.h
index b241fdc97b2..37d52b8d29d 100644
--- a/contrib/pageinspect/pageinspect.h
+++ b/contrib/pageinspect/pageinspect.h
@@ -13,7 +13,9 @@
 #ifndef _PAGEINSPECT_H_
 #define _PAGEINSPECT_H_
 
+#include "common/relpath.h"
 #include "storage/bufpage.h"
+#include "utils/relcache.h"
 
 /*
  * Extension version number, for supporting older extension versions' objects
@@ -26,5 +28,6 @@ enum pageinspect_version
 
 /* in rawpage.c */
 extern Page get_page_from_raw(bytea *raw_page);
+extern void check_relation_accessible(Relation rel, ForkNumber forknum);
 
 #endif							/* _PAGEINSPECT_H_ */
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593edb2..3b65c070d07 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -17,6 +17,7 @@
 
 #include "access/htup_details.h"
 #include "access/relation.h"
+#include "access/xlog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
@@ -165,15 +166,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 						RelationGetRelationName(rel)),
 				 errdetail_relkind_not_supported(rel->rd_rel->relkind)));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel, forknum);
 
 	if (blkno >= RelationGetNumberOfBlocksInFork(rel, forknum))
 		ereport(ERROR,
@@ -200,6 +193,35 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 	return raw_page;
 }
 
+/*
+ * check_relation_accessible
+ *
+ * Check that the given fork of a relation can be read by this session.
+ */
+void
+check_relation_accessible(Relation rel, ForkNumber forknum)
+{
+	/*
+	 * Reject attempts to read non-local temporary relations; we would be
+	 * likely to get wrong data since we have no visibility into the owning
+	 * session's local buffers.
+	 */
+	if (RELATION_IS_OTHER_TEMP(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot access temporary tables of other sessions")));
+
+	/*
+	 * Temporary and unlogged relations are inaccessible during recovery,
+	 * except for the init fork of an unlogged relation, which does exist.
+	 */
+	if (!RelationIsPermanent(rel) && RecoveryInProgress() &&
+		forknum != INIT_FORKNUM)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot access temporary or unlogged relations during recovery")));
+}
+
 
 /*
  * get_page_from_raw
-- 
2.56.0

From 11068d11afd02e62acaece1fae7ce4d9a4fce743 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Fri, 6 Nov 2026 14:28:09 +0100
Subject: [PATCH v5 2/2] pgstattuple: Reject unlogged relations during recovery

Unlogged tables and indexes have no data on a standby, so reading one
there must fail.  pgstattuple(), pgstattuple_approx(), pg_relpages(),
pgstatindex(), pgstatginindex() and pgstathashindex() read the relation
directly instead of going through the planner, so on a standby they
failed with a confusing low-level message instead of the error that
SELECT reports:

  ERROR:  could not open file "base/5/16384": No such file or directory

Check for this case and report the same error as SELECT:

  ERROR:  cannot access temporary or unlogged relations during recovery

This check and the existing check against temporary relations of other
sessions are now grouped in a new routine, check_relation_accessible(),
used by all these functions except pg_relpages().  pg_relpages() keeps
only the recovery check, as it works on temporary tables of other
sessions.

The error for temporary relations of other sessions now says "indexes"
or "tables" depending on the relation kind.  Previously pgstatindex()
and pgstattuple() said "tables" even for an index, while
pgstatginindex() and pgstathashindex() said "indexes".

Author: Jim Jones <[email protected]>
Reported-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kirill Reshke <[email protected]>
Reviewed-by: Michael Paquier <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Discussion: https://www.postgresql.org/message-id/flat/[email protected]
---
 contrib/pgstattuple/pgstatapprox.c | 11 ++------
 contrib/pgstattuple/pgstatindex.c  | 42 ++++++++++--------------------
 contrib/pgstattuple/pgstattuple.c  | 38 ++++++++++++++++++++++-----
 contrib/pgstattuple/pgstattuple.h  | 21 +++++++++++++++
 4 files changed, 69 insertions(+), 43 deletions(-)
 create mode 100644 contrib/pgstattuple/pgstattuple.h

diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..32dcc50586f 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -20,6 +20,7 @@
 #include "commands/vacuum.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/freespace.h"
 #include "storage/procarray.h"
@@ -322,15 +323,7 @@ pgstattuple_approx_internal(Oid relid, FunctionCallInfo fcinfo)
 
 	rel = relation_open(relid, AccessShareLock);
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel);
 
 	/*
 	 * We support only relation kinds with a visibility map and a free space
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..56e2e09c1f2 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -32,10 +32,12 @@
 #include "access/htup_details.h"
 #include "access/nbtree.h"
 #include "access/relation.h"
+#include "access/xlog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/read_stream.h"
 #include "utils/rel.h"
@@ -228,15 +230,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 				 errmsg("relation \"%s\" is not a btree index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel);
 
 	/*
 	 * A !indisready index could lead to ERRCODE_DATA_CORRUPTED later, so exit
@@ -482,7 +476,15 @@ pg_relpages_impl(Relation rel)
 						RelationGetRelationName(rel)),
 				 errdetail_relkind_not_supported(rel->rd_rel->relkind)));
 
-	/* note: this will work OK on non-local temp tables */
+	/*
+	 * Temporary tables of other sessions are not rejected, as only the
+	 * relation size is needed, but temporary and unlogged relations are
+	 * inaccessible during recovery.
+	 */
+	if (!RelationIsPermanent(rel) && RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot access temporary or unlogged relations during recovery")));
 
 	relpages = RelationGetNumberOfBlocks(rel);
 
@@ -547,15 +549,7 @@ pgstatginindex_internal(Oid relid, FunctionCallInfo fcinfo)
 				 errmsg("relation \"%s\" is not a GIN index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary indexes of other sessions")));
+	check_relation_accessible(rel);
 
 	/* see pgstatindex_impl */
 	if (!rel->rd_index->indisvalid)
@@ -637,15 +631,7 @@ pgstathashindex(PG_FUNCTION_ARGS)
 				 errmsg("relation \"%s\" is not a hash index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary indexes of other sessions")));
+	check_relation_accessible(rel);
 
 	/* see pgstatindex_impl */
 	if (!rel->rd_index->indisvalid)
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..2a3ce1b32b2 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -30,10 +30,12 @@
 #include "access/nbtree.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
+#include "access/xlog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am_d.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "utils/varlena.h"
@@ -237,22 +239,46 @@ pgstattuplebyid_v1_5(PG_FUNCTION_ARGS)
 }
 
 /*
- * pgstat_relation
+ * check_relation_accessible
+ *
+ * Check that a relation can be read by this session.
  */
-static Datum
-pgstat_relation(Relation rel, FunctionCallInfo fcinfo)
+void
+check_relation_accessible(Relation rel)
 {
-	const char *err;
-
 	/*
 	 * Reject attempts to read non-local temporary relations; we would be
 	 * likely to get wrong data since we have no visibility into the owning
 	 * session's local buffers.
 	 */
 	if (RELATION_IS_OTHER_TEMP(rel))
+	{
+		if (rel->rd_rel->relkind == RELKIND_INDEX)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot access temporary indexes of other sessions")));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot access temporary tables of other sessions")));
+	}
+
+	/* Temporary and unlogged relations are inaccessible during recovery. */
+	if (!RelationIsPermanent(rel) && RecoveryInProgress())
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+				 errmsg("cannot access temporary or unlogged relations during recovery")));
+}
+
+/*
+ * pgstat_relation
+ */
+static Datum
+pgstat_relation(Relation rel, FunctionCallInfo fcinfo)
+{
+	const char *err;
+
+	check_relation_accessible(rel);
 
 	if (RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind) ||
 		rel->rd_rel->relkind == RELKIND_SEQUENCE)
diff --git a/contrib/pgstattuple/pgstattuple.h b/contrib/pgstattuple/pgstattuple.h
new file mode 100644
index 00000000000..1617b23f901
--- /dev/null
+++ b/contrib/pgstattuple/pgstattuple.h
@@ -0,0 +1,21 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgstattuple.h
+ *	  Common functions for pgstattuple.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  contrib/pgstattuple/pgstattuple.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _PGSTATTUPLE_H_
+#define _PGSTATTUPLE_H_
+
+#include "utils/relcache.h"
+
+/* in pgstattuple.c */
+extern void check_relation_accessible(Relation rel);
+
+#endif							/* _PGSTATTUPLE_H_ */
-- 
2.56.0

Reply via email to