From 95f3471c8d97bbfb17314035b9583eba6bbb2b9e Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
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 <jim.jones@uni-muenster.de>
Co-authored-by: Kirill Reshke <reshkekirill@gmail.com>
Co-authored-by: Surya Poondla <suryapoondla4@gmail.com>
Reported-by: Zsolt Parragi <zsolt.parragi@percona.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://www.postgresql.org/message-id/flat/82ba0876-6127-4770-b900-aa1bd52abfed@uni-muenster.de
---
 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

