On 23/03/2026 11:22, Soumya S Murali wrote:
Overall the patch LGTM.


This is a step forward in really isolating contents of temp tables from
other sessions, but the more I think about it, the more I'm concerned
with the current approach -- I spent some time investigating this
problem a bit deeper last week.

My main concern is the usage of gram.y, as a parser is arguably fragile
for this kind of things. For instance, one can always change the
search_path and bypass this restriction:

(table t was created in a different session)

postgres=# SELECT * FROM pg_temp_81.t;
ERROR:  cannot access temporary relations of other sessions
LINE 1: SELECT * FROM pg_temp_81.t;
                      ^
postgres=# SET search_path = pg_temp_81, public;
SET
postgres=# SELECT * FROM t;
 ?column?
----------
(0 rows)

* See: if (relation->relpersistence == RELPERSISTENCE_TEMP) in
namespace.c for more details.

IMO, since it is an access control issue, I guess we better treat it as
such and modify aclchk.c instead.

Something like this the file attached. This breaks an unrelated test,
which is potentially a bug in REPACK ... but I'll describe it in another
thread.

Thoughts?

Best, Jim
From aca4001c8d79b6b298ab77832ead35e5ca9c3658 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Mon, 23 Mar 2026 14:20:24 +0100
Subject: [PATCH v13] Prevent superusers from accessing temp tables of other
 sessions

---
 src/backend/catalog/aclchk.c                  |  40 +++++
 src/test/modules/test_misc/meson.build        |   1 +
 .../test_misc/t/011_temp_obj_multisession.pl  | 142 ++++++++++++++++++
 3 files changed, 183 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/011_temp_obj_multisession.pl

diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0..7ffcfce8d2 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -48,6 +48,7 @@
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
@@ -3350,6 +3351,21 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode 
mask,
                !superuser_arg(roleid))
                mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | 
ACL_USAGE);
 
+       /*
+        * Enforce temporary table isolation: prevent access to other sessions'
+        * temporary tables, even for superusers.
+        */
+       if (classForm->relpersistence == RELPERSISTENCE_TEMP &&
+               isOtherTempNamespace(classForm->relnamespace))
+       {
+               ReleaseSysCache(tuple);
+               if (is_missing != NULL)
+                       return 0;                       /* no privileges, but 
don't treat as missing */
+               ereport(ERROR,
+                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                errmsg("cannot access temporary relations of 
other sessions")));
+       }
+
        /*
         * Otherwise, superusers bypass all permission-checking.
         */
@@ -4135,6 +4151,30 @@ object_ownercheck(Oid classid, Oid objectid, Oid roleid)
        SysCacheIdentifier cacheid;
        Oid                     ownerId;
 
+       /*
+        * For relations, block access to other sessions' temporary tables 
before
+        * checking superuser status.  Temp tables are session-private by 
design;
+        * superuser access would allow cross-session data leakage.
+        */
+       if (classid == RelationRelationId)
+       {
+               HeapTuple tup;
+               Form_pg_class relform;
+
+               tup = SearchSysCache1(RELOID, ObjectIdGetDatum(objectid));
+               if (HeapTupleIsValid(tup))
+               {
+                       relform = (Form_pg_class) GETSTRUCT(tup);
+                       if (relform->relpersistence == RELPERSISTENCE_TEMP &&
+                               isOtherTempNamespace(relform->relnamespace))
+                       {
+                               ReleaseSysCache(tup);
+                               return false;
+                       }
+                       ReleaseSysCache(tup);
+               }
+       }
+
        /* Superusers bypass all permission checking. */
        if (superuser_arg(roleid))
                return true;
diff --git a/src/test/modules/test_misc/meson.build 
b/src/test/modules/test_misc/meson.build
index 6e8db1621a..356121673a 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
       't/008_replslot_single_user.pl',
       't/009_log_temp_files.pl',
       't/010_index_concurrently_upsert.pl',
+      't/011_temp_obj_multisession.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_temp_obj_multisession.pl 
b/src/test/modules/test_misc/t/011_temp_obj_multisession.pl
new file mode 100644
index 0000000000..d1b3dbc1e0
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_temp_obj_multisession.pl
@@ -0,0 +1,142 @@
+# 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;));
+
+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;",
+    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 access temporary relations 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');
+
+# ALTER TABLE .. RENAME TEMPORARY TABLE from other session
+$node->psql(
+    'postgres',
+    "ALTER TABLE $tempschema.foo RENAME TO bar;",
+    stderr => \$stderr
+);
+like($stderr, qr/must be owner of table foo/,
+     'ALTER TABLE ... RENAME on other session temp table is blocked');
+
+# ALTER TABLE .. ADD COLUMN in TEMPORARY TABLE from other session
+$node->psql(
+    'postgres',
+    "ALTER TABLE $tempschema.foo ADD COLUMN bar int;",
+    stderr => \$stderr
+);
+like($stderr, qr/must be owner of table foo/,
+     'ALTER TABLE ... ADD COLUMN on other session temp table is blocked');
+
+# COPY TEMPORARY TABLE from other session
+$node->psql(
+    'postgres',
+    "COPY $tempschema.foo TO '/tmp/x';",
+    stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+     'COPY on other session temp table is blocked');
+
+# LOCK TEMPORARY TABLE from other session
+$node->psql(
+    'postgres',
+    "BEGIN; LOCK TABLE $tempschema.foo IN ACCESS EXCLUSIVE MODE;",
+    stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+     'LOCK on other session temp table is blocked');
+
+# Access via search_path manipulation (unqualified name)
+$node->psql(
+    'postgres',
+    "SET search_path = $tempschema, public; SELECT val FROM foo;",
+    stderr => \$stderr
+);
+like($stderr, qr/cannot access temporary relations of other sessions/,
+     'SELECT via search_path manipulation on other session temp table is 
blocked');
+
+# has_table_privilege() should return false
+my $result = $node->safe_psql(
+    'postgres',
+    "SELECT has_table_privilege('$tempschema.foo'::regclass, 'SELECT');"
+);
+is($result, 'f', 'has_table_privilege returns false for other session temp 
table');
+
+# 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

Reply via email to