Hi,

Recently, we have added two commits ([1], [2]) that are fixing a bug with
accessing temporary tables of other sessions. I found out that this bug didn't
go away completely :

== session 1 ==

postgres=# CREATE TABLE empty_table (id INT);
CREATE TABLE

== session 2 ==

postgres=# INSERT INTO pg_temp_0.empty_table VALUES (1);
INSERT 0 1

As you can see, the INSERT command completes successfully. This happens because
empty_table has no pages, so the insert path looks like this :
heap_insert -> RelationGetBufferForTuple -> RelationAddBlocks -> ....

The RelationAddBlocks extends relations's smgr and allocates a new temp buffer
without calling ReadBufferExtended. Thus, we are 1) bypassing the
"RELATION_IS_OTHER_TEMP" check and 2) creating a buffer in our own temp buffers
pool. The (2) will lead to an error [3] if we attempt to flush such a buffer.

I suggest fixing it by checking whether the relation is other-temp-rel inside
the ExtendBufferedRelLocal function. As far as I can see, all
temp-relation-extend paths include this function.

Please, see the attached patch that fixes a problem and adds a new test.

[1] 40927d458fe1a8c96bcf418b47169f0f6eb15946
[2] ce146621f7860d2e19c509f1466feca3bf777678
[3] ERROR: could not open file "base/5/t3_16386": No such file or directory

P.S.
Jim, I attach you in CC of this thread since you are the co-author of the
original fix. I hope you don't mind :)

--
Best regards,
Daniil Davydov
From 8420c65d4e23329f3a323b7caf95e36f1689a715 Mon Sep 17 00:00:00 2001
From: Daniil Davidov <[email protected]>
Date: Wed, 3 Jun 2026 19:35:04 +0700
Subject: [PATCH] Prevent access to other sessions' empty temp tables

Commit ce146621 ensures that ERROR is raised if session tryes to read
pages of other session's temp table. But there is corner case, when
other temp table is empty - in this case the INSERT command will bypass
our checks and executes without any errors.

Such a behavior is inconsistent and erroneous, because it leaves an invalid
buffer in the temp buffers pool : since buffer was created for other
temp table, we will face an error "no such file or directory" while trying
to flush this buffer.

This commit fixes it by adding a RELATION_IS_OTHER_TEMP check in the
relation-extension path.
---
 src/backend/storage/buffer/localbuf.c            | 11 +++++++++++
 src/include/utils/rel.h                          |  6 +++---
 .../test_misc/t/013_temp_obj_multisession.pl     | 16 ++++++++++++++++
 3 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e13d0..a5642583333 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -363,6 +363,17 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 	BlockNumber first_block;
 	instr_time	io_start;
 
+	/*
+	 * Reject attempts to extend non-local temporary relations; we have no
+	 * ability to transfer about-to-be-created local buffers into the owning
+	 * session's local buffers.  This is the canonical place for the check,
+	 * covering any attempt to extend local relation.
+	 */
+	if (bmr.rel && RELATION_IS_OTHER_TEMP(bmr.rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot access temporary tables of other sessions")));
+
 	/* Initialize local buffers if first request in this session */
 	if (LocalBufHash == NULL)
 		InitLocalBuffers();
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8ff7..81493d00c47 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -668,9 +668,9 @@ RelationCloseSmgr(Relation relation)
  * the owning session keeps the data in its private local buffer pool,
  * which we cannot access.  Existing buffer-manager entry points
  * (ReadBuffer_common(), StartReadBuffersImpl(), read_stream_begin_impl(),
- * and PrefetchBuffer()) already enforce this; any new buffer-access entry
- * points must do the same.  Command-level code (TRUNCATE, ALTER TABLE,
- * VACUUM, CLUSTER, REINDEX, ...) additionally uses this macro for
+ * PrefetchBuffer() and ExtendBufferedRelLocal()) already enforce this; any new
+ * buffer-access entry points must do the same.  Command-level code (TRUNCATE,
+ * ALTER TABLE, VACUUM, CLUSTER, REINDEX, ...) additionally uses this macro for
  * command-specific error messages.
  *
  * Beware of multiple eval of argument
diff --git a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
index 5f3cc7d2fc5..ff6f23ef3b1 100644
--- a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
+++ b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
@@ -36,6 +36,10 @@ my $psql1 = $node->background_psql('postgres');
 # masked by an index scan that would hit ReadBuffer_common from nbtree.
 $psql1->query_safe(q(CREATE TEMP TABLE foo AS SELECT 42 AS val;));
 
+# Also create an empty table, so read path go straight through the
+# extend-relation entry point.
+$psql1->query_safe(q(CREATE TEMP TABLE empty_foo (val INT);));
+
 # Resolve the owner's temp schema so the probing session can refer to
 # the table by a fully-qualified name.
 my $tempschema = $node->safe_psql(
@@ -66,6 +70,18 @@ like(
 	qr/cannot access temporary tables of other sessions/,
 	'SELECT (seqscan via read_stream)');
 
+# INSERT into empty table goes through hio.c which calls RelationAddBlocks() to
+# extend the table; that hits the check before new pages are created for the
+# table.
+$node->psql(
+	'postgres',
+	"INSERT INTO $tempschema.empty_foo VALUES (42);",
+	stderr => \$stderr);
+like(
+	$stderr,
+	qr/cannot access temporary tables of other sessions/,
+	'INSERT (caught via hio.c)');
+
 # INSERT goes through hio.c which calls ReadBufferExtended() to find a
 # page with free space; that hits the existing check before any data
 # is written.
-- 
2.43.0

Reply via email to