From 6a3562b4ac8917c8b577797e5468416a90cc04f5 Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Thu, 18 Sep 2025 17:24:09 +0530
Subject: [PATCH] Negative RelfilenumberMap cache entries from
 pg_filenode_relation()

RelidByRelfilenumber() adds negative entries to the cache. It has three
users, logical replication, autoprewarm and pg_filenode_relation(). The
first two need negative entries in the cache in case they happen to
lookup non-existent mapping again and again.  However such mappings will
be smaller in number and usually come from some database object e.g. WAL
or autoprewarm metadata.

But pg_filenode_relation(), which is SQL callable, may be invoked many
times with invalid tablespace and relfilenode pairs, causing the cache
to be bloated with negative cache entries. This can be used as a denial
of service attack since any user can execute it. This commit avoids such
a bloat.

Reported by: Ashutosh Bapat
Author: Ashutosh Bapat
---
 contrib/pg_prewarm/autoprewarm.c               |  6 +++++-
 .../replication/logical/reorderbuffer.c        |  7 ++++++-
 src/backend/utils/adt/dbsize.c                 |  6 +++++-
 src/backend/utils/cache/relfilenumbermap.c     | 18 +++++++++++++-----
 src/include/utils/relfilenumbermap.h           |  3 ++-
 5 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index 8b68dafc261..9732495eaa4 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -540,7 +540,11 @@ autoprewarm_database_main(Datum main_arg)
 
 		StartTransactionCommand();
 
-		reloid = RelidByRelfilenumber(blk.tablespace, blk.filenumber);
+		/*
+		 * Cache negative entries in case we end up looking up a non-existent
+		 * mapping again and again.
+		 */
+		reloid = RelidByRelfilenumber(blk.tablespace, blk.filenumber, true);
 		if (!OidIsValid(reloid) ||
 			(rel = try_relation_open(reloid, AccessShareLock)) == NULL)
 		{
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4736f993c37..b85f426e19f 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -2326,8 +2326,13 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				case REORDER_BUFFER_CHANGE_DELETE:
 					Assert(snapshot_now);
 
+					/*
+					 * Cache negative entries in case we end up looking up a
+					 * non-existent mapping again and again.
+					 */
 					reloid = RelidByRelfilenumber(change->data.tp.rlocator.spcOid,
-												  change->data.tp.rlocator.relNumber);
+												  change->data.tp.rlocator.relNumber,
+												  true);
 
 					/*
 					 * Mapped catalog tuple without data, emitted while
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 894d226541f..8bf22daa4c2 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -955,7 +955,11 @@ pg_filenode_relation(PG_FUNCTION_ARGS)
 	if (!RelFileNumberIsValid(relfilenumber))
 		PG_RETURN_NULL();
 
-	heaprel = RelidByRelfilenumber(reltablespace, relfilenumber);
+	/*
+	 * A malicious user invoke the SQL function with negative entries many
+	 * times to consume memory as a DoS attach. Avoid that.
+	 */
+	heaprel = RelidByRelfilenumber(reltablespace, relfilenumber, false);
 
 	if (!OidIsValid(heaprel))
 		PG_RETURN_NULL();
diff --git a/src/backend/utils/cache/relfilenumbermap.c b/src/backend/utils/cache/relfilenumbermap.c
index 0b6f9cf3fa1..0aed3058350 100644
--- a/src/backend/utils/cache/relfilenumbermap.c
+++ b/src/backend/utils/cache/relfilenumbermap.c
@@ -135,10 +135,13 @@ InitializeRelfilenumberMap(void)
  * identify a temporary relation would require a backend's proc number, which
  * we do not know about.  Hence, this function ignores this case.
  *
+ * If cache_negative is true, negative cache entries (mapping to InvalidOid)
+ * will be stored in the cache. If false, they won't be cached.
+ *
  * Returns InvalidOid if no relation matching the criteria could be found.
  */
 Oid
-RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber)
+RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber, bool cache_negative)
 {
 	RelfilenumberMapKey key;
 	RelfilenumberMapEntry *entry;
@@ -239,11 +242,16 @@ RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber)
 	 * Only enter entry into cache now, our opening of pg_class could have
 	 * caused cache invalidations to be executed which would have deleted a
 	 * new entry if we had entered it above.
+	 *
+	 * Cache valid entries always, and negative entries only if requested.
 	 */
-	entry = hash_search(RelfilenumberMapHash, &key, HASH_ENTER, &found);
-	if (found)
-		elog(ERROR, "corrupted hashtable");
-	entry->relid = relid;
+	if (OidIsValid(relid) || cache_negative)
+	{
+		entry = hash_search(RelfilenumberMapHash, &key, HASH_ENTER, &found);
+		if (found)
+			elog(ERROR, "corrupted hashtable");
+		entry->relid = relid;
+	}
 
 	return relid;
 }
diff --git a/src/include/utils/relfilenumbermap.h b/src/include/utils/relfilenumbermap.h
index 86fadefe499..5799a3257d3 100644
--- a/src/include/utils/relfilenumbermap.h
+++ b/src/include/utils/relfilenumbermap.h
@@ -16,6 +16,7 @@
 #include "common/relpath.h"
 
 extern Oid	RelidByRelfilenumber(Oid reltablespace,
-								 RelFileNumber relfilenumber);
+								 RelFileNumber relfilenumber,
+								 bool cache_negative);
 
 #endif							/* RELFILENUMBERMAP_H */

base-commit: 2e66cae935c2e0f7ce9bab6b65ddeb7806f4de7c
-- 
2.34.1

