Hi,
I did some digging:
It seems that b7b0f3f2724 is what actually broke it. By switching from
ReadBufferExtended() to read_stream_next_buffer(), it silently routed
all SELECT/UPDATE/DELETE/COPY away from the check that was sitting in
ReadBufferExtended().
@@ -528,25 +599,23 @@ heap_fetch_next_buffer(HeapScanDesc scan,
ScanDirection dir)
*/
CHECK_FOR_INTERRUPTS();
- if (unlikely(!scan->rs_inited))
+ /*
+ * If the scan direction is changing, reset the prefetch block
to the
+ * current block. Otherwise, we will incorrectly prefetch the blocks
+ * between the prefetch block and the current block again before
+ * prefetching blocks in the new, correct scan direction.
+ */
+ if (unlikely(scan->rs_dir != dir))
{
- scan->rs_cblock = heapgettup_initial_block(scan, dir);
+ scan->rs_prefetch_block = scan->rs_cblock;
+ read_stream_reset(scan->rs_read_stream);
+ }
- /* ensure rs_cbuf is invalid when we get
InvalidBlockNumber */
- Assert(scan->rs_cblock != InvalidBlockNumber ||
- !BufferIsValid(scan->rs_cbuf));
+ scan->rs_dir = dir;
- scan->rs_inited = true;
- }
- else
- scan->rs_cblock = heapgettup_advance_block(scan,
scan->rs_cblock,
-
dir);
-
- /* read block if valid */
- if (BlockNumberIsValid(scan->rs_cblock))
- scan->rs_cbuf = ReadBufferExtended(scan->rs_base.rs_rd,
MAIN_FORKNUM,
-
scan->rs_cblock, RBM_NORMAL,
-
scan->rs_strategy);
+ scan->rs_cbuf = read_stream_next_buffer(scan->rs_read_stream, NULL);
+ if (BufferIsValid(scan->rs_cbuf))
+ scan->rs_cblock = BufferGetBlockNumber(scan->rs_cbuf);
}
It simply builds upon 210622c60e1 (introduces StartReadBuffers ->
PinBufferForBlock) and b5a9b18cd0b (builds read_stream_begin_relation on
top of it).
So I thought we can explore the option of adding this check directly in
read_stream_begin_relation():
if (RELATION_IS_OTHER_TEMP(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot access temporary relations of other sessions")));
Thoughts?
See v15 attached.
Daniil, feel free to revert it to your last patch if you disagree with
this approach.
Best, Jim
From bd15be73156acc258fd06c7b77a875fb2e154fe8 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Wed, 8 Apr 2026 14:53:06 +0200
Subject: [PATCH v15 1/2] Prevent access to other sessions' temporary tables
---
src/backend/storage/aio/read_stream.c | 10 ++++++++++
src/backend/storage/buffer/bufmgr.c | 14 ++++++++++++--
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index b6fce4e7cc6..e64c74040e8 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -972,6 +972,16 @@ read_stream_begin_relation(int flags,
void *callback_private_data,
size_t per_buffer_data_size)
{
+ /*
+ * 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 relations of other sessions")));
+
return read_stream_begin_impl(flags,
strategy,
rel,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 3cc0b0bdd92..4c464d51f3c 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -795,7 +795,7 @@ PrefetchBuffer(Relation reln, ForkNumber forkNum, BlockNumber blockNum)
if (RELATION_IS_OTHER_TEMP(reln))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot access temporary tables of other sessions")));
+ errmsg("cannot access temporary relations of other sessions")));
/* pass it off to localbuf.c */
return PrefetchLocalBuffer(RelationGetSmgr(reln), forkNum, blockNum);
@@ -936,7 +936,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
if (RELATION_IS_OTHER_TEMP(reln))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot access temporary tables of other sessions")));
+ errmsg("cannot access temporary relations of other sessions")));
/*
* Read the buffer, and update pgstat counters to reflect a cache hit or
@@ -1317,6 +1317,16 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
else
persistence = smgr_persistence;
+ /*
+ * Safety net for callers (e.g. via ExtendBufferedRelTo) that reach here
+ * without going through ReadBufferExtended. See ReadBufferExtended for
+ * the primary check and full explanation.
+ */
+ if (rel != NULL && RELATION_IS_OTHER_TEMP(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot access temporary relations of other sessions")));
+
if (unlikely(mode == RBM_ZERO_AND_CLEANUP_LOCK ||
mode == RBM_ZERO_AND_LOCK))
{
--
2.43.0
From 3a463f9c3eff8aab838efe1b571a9117b96106cd Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Wed, 8 Apr 2026 14:56:57 +0200
Subject: [PATCH v15 2/2] Test cross-session access on temporary tables
---
src/test/modules/test_misc/meson.build | 1 +
.../test_misc/t/012_temp_obj_multisession.pl | 117 ++++++++++++++++++
2 files changed, 118 insertions(+)
create mode 100644 src/test/modules/test_misc/t/012_temp_obj_multisession.pl
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 1b25d98f7f3..a54599cc301 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -20,6 +20,7 @@ tests += {
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
't/011_lock_stats.pl',
+ 't/012_temp_obj_multisession.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/012_temp_obj_multisession.pl b/src/test/modules/test_misc/t/012_temp_obj_multisession.pl
new file mode 100644
index 00000000000..4d599152740
--- /dev/null
+++ b/src/test/modules/test_misc/t/012_temp_obj_multisession.pl
@@ -0,0 +1,117 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::BackgroundPsql;
+use Test::More;
+
+# Set up a fresh node
+my $node = PostgreSQL::Test::Cluster->new('temp_lock');
+$node->init;
+$node->start;
+
+# Create a long-lived session
+my $psql1 = $node->background_psql('postgres');
+
+$psql1->query_safe(
+ q(CREATE TEMP TABLE foo AS SELECT 42 AS val;));
+
+# Create an index so the index-scan path (ReadBufferExtended) can be tested
+$psql1->query_safe(
+ q(CREATE INDEX ON foo(val);));
+
+my $tempschema = $node->safe_psql(
+ 'postgres',
+ q{
+ SELECT n.nspname
+ FROM pg_class c
+ JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE relname = 'foo' AND relpersistence = 't';
+ }
+);
+chomp $tempschema;
+ok($tempschema =~ /^pg_temp_\d+$/, "got temp schema: $tempschema");
+
+
+# SELECT TEMPORARY TABLE from other session
+my ($stdout, $stderr);
+$node->psql(
+ 'postgres',
+ "SELECT val FROM $tempschema.foo;",
+ stdout => \$stdout,
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'SELECT on other session temp table is not allowed');
+
+# UPDATE TEMPORARY TABLE from other session
+$node->psql(
+ 'postgres',
+ "UPDATE $tempschema.foo SET val = NULL;",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'UPDATE on other session temp table is not allowed');
+
+# DELETE records from TEMPORARY TABLE from other session
+$node->psql(
+ 'postgres',
+ "DELETE FROM $tempschema.foo;",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'DELETE on other session temp table is not allowed');
+
+# TRUNCATE TEMPORARY TABLE from other session
+$node->psql(
+ 'postgres',
+ "TRUNCATE TABLE $tempschema.foo;",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot truncate temporary tables of other sessions/,
+ 'TRUNCATE on other session temp table is not allowed');
+
+# INSERT INTO TEMPORARY TABLE from other session
+$node->psql(
+ 'postgres',
+ "INSERT INTO $tempschema.foo VALUES (73);",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'INSERT INTO on other session temp table is not allowed');
+
+# COPY TEMPORARY TABLE from other session
+$node->psql(
+ 'postgres',
+ "COPY $tempschema.foo TO STDOUT;",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'COPY on other session temp table is blocked');
+
+# SELECT via index scan from other session.
+# Sequential scans are blocked at read_stream_begin_relation(); index scans
+# bypass that path entirely and reach ReadBufferExtended() in bufmgr.c
+# (nbtree's _bt_getbuf calls ReadBuffer directly for individual page fetches).
+# enable_seqscan=off forces the planner to use the index.
+$node->psql(
+ 'postgres',
+ "SET enable_seqscan = off; SELECT val FROM $tempschema.foo WHERE val = 42;",
+ stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+ 'index scan on other session temp table is not allowed (exercises ReadBufferExtended path)');
+
+# DROP TEMPORARY TABLE from other session
+my $ok = $node->psql(
+ 'postgres',
+ "DROP TABLE $tempschema.foo;"
+);
+ok($ok == 0, 'DROP TABLE executed successfully');
+
+# Clean up
+$psql1->quit;
+
+done_testing();
--
2.43.0