On Mon, Apr 14, 2025 at 12:36 PM Daniil Davydov <3daniss...@gmail.com> wrote:
> Hi, > > During previous commitfest this topic already has been discussed > within the "Forbid to DROP temp tables of other sessions" thread [1]. > Unfortunately its name doesn't reflect the real problem, so I decided > to start a new thread, as David G. Johnston advised. > > Here are the summary results of the discussion [1] : > The superuser is only allowed to DROP temporary relations of other > sessions. Other commands (like SELECT, INSERT, UPDATE, DELETE ...) > must be forbidden to him. Error message for this case will look like > this : `could not access temporary relations of other sessions`. > For now, superuser still can specify such operations because of a bug > in the code that mistakenly recognizes other session's temp table as > permanent (I've covered this topic in more detail in [2]). Attached > patch fixes this bug (targeted on > b51f86e49a7f119004c0ce5d0be89cdf98309141). > > Opened issue: > Not everyone liked the idea that table's persistence can be assigned > to table during makeRangeVarXXX calls (inside gram.y). > My opinion - `As far as "pg_temp_" prefix is reserved by the postgres > kernel, we can definitely say that we have encountered a temporary > table when we see this prefix.` > > I will be glad to hear your opinion. > > -- > Best regards, > Daniil Davydov > > [1] > https://www.postgresql.org/message-id/CAJDiXgj72Axj0d4ojKdRWG_rnkfs4uWY414NL%3D15sCvh7-9rwg%40mail.gmail.com > [2] > https://www.postgresql.org/message-id/CAJDiXgj%2B5UKLWSUT5605rJhuw438NmEKecvhFAF2nnrMsgGK3w%40mail.gmail.com Hi Daniil, Your patch for securing cross-session temp table access is a great improvement. The RVR_OTHER_TEMP_OK flag elegantly handles the DROP case while keeping the main restriction in place. For schema name validation, an exact strcmp for "pg_temp" and proper numeric parsing for "pg_temp_X" would be more precise than the current prefix check. This would avoid any accidental matches to similarly named schemas. The error message could be adjusted to emphasize permissions, like "permission denied for cross-session temp table access". This would make the security intent clearer to users. I noticed the Assert assumes myTempNamespace is always valid. While correct, a brief comment explaining why this is safe would help future maintainers. The relpersistence logic could also be centralized in one place for consistency. I've added an isolation test to verify the behavior when trying to access another backend's temp tables. It confirms the restrictions work as intended while allowing permitted operations. Thanks for working on this important security enhancement! Best regards, Stepan Neretin
From da27bc190faab3853f6a2cc0748f1f5476215001 Mon Sep 17 00:00:00 2001 From: Daniil Davidov <d.davy...@postgrespro.ru> Date: Mon, 14 Apr 2025 11:01:56 +0700 Subject: [PATCH v6 1/2] Fix accessing other sessions temp tables --- src/backend/catalog/namespace.c | 56 ++++++++++++++++++++------------ src/backend/commands/tablecmds.c | 3 +- src/backend/nodes/makefuncs.c | 6 +++- src/backend/parser/gram.y | 11 ++++++- src/include/catalog/namespace.h | 2 ++ 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..f407efd9447 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -499,28 +499,44 @@ RangeVarGetRelidExtended(const RangeVar *relation, LOCKMODE lockmode, */ if (relation->relpersistence == RELPERSISTENCE_TEMP) { - if (!OidIsValid(myTempNamespace)) - relId = InvalidOid; /* this probably can't happen? */ - else - { - if (relation->schemaname) - { - Oid namespaceId; + Oid namespaceId; - namespaceId = LookupExplicitNamespace(relation->schemaname, missing_ok); + if (relation->schemaname) + { + namespaceId = LookupExplicitNamespace(relation->schemaname, missing_ok); + /* + * If the user has specified an existing temporary schema + * owned by another user. + */ + if (OidIsValid(namespaceId) && namespaceId != myTempNamespace) + { /* - * For missing_ok, allow a non-existent schema name to - * return InvalidOid. + * We don't allow users to access temp tables of other + * sessions except for the case of dropping tables. */ - if (namespaceId != myTempNamespace) + if (!(flags & RVR_OTHER_TEMP_OK)) ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("temporary tables cannot specify a schema name"))); + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("could not access temporary relations of other sessions"))); } + } + else + { + namespaceId = myTempNamespace; - relId = get_relname_relid(relation->relname, myTempNamespace); + /* + * If this table was recognized as temporary, it means that we + * found it because backend's temporary namespace was specified + * in search_path. Thus, MyTempNamespace must contain valid oid. + */ + Assert(OidIsValid(namespaceId)); } + + if (missing_ok && !OidIsValid(namespaceId)) + relId = InvalidOid; + else + relId = get_relname_relid(relation->relname, namespaceId); } else if (relation->schemaname) { @@ -3553,21 +3569,19 @@ get_namespace_oid(const char *nspname, bool missing_ok) RangeVar * makeRangeVarFromNameList(const List *names) { - RangeVar *rel = makeRangeVar(NULL, NULL, -1); + RangeVar *rel; switch (list_length(names)) { case 1: - rel->relname = strVal(linitial(names)); + rel = makeRangeVar(NULL, strVal(linitial(names)), -1); break; case 2: - rel->schemaname = strVal(linitial(names)); - rel->relname = strVal(lsecond(names)); + rel = makeRangeVar(strVal(linitial(names)), strVal(lsecond(names)), -1); break; case 3: + rel = makeRangeVar(strVal(lsecond(names)), strVal(lthird(names)), -1); rel->catalogname = strVal(linitial(names)); - rel->schemaname = strVal(lsecond(names)); - rel->relname = strVal(lthird(names)); break; default: ereport(ERROR, @@ -3774,6 +3788,8 @@ GetTempNamespaceProcNumber(Oid namespaceId) return INVALID_PROC_NUMBER; /* no such namespace? */ if (strncmp(nspname, "pg_temp_", 8) == 0) result = atoi(nspname + 8); + else if (strcmp(nspname, "pg_temp") == 0) + result = MyProcNumber; else if (strncmp(nspname, "pg_toast_temp_", 14) == 0) result = atoi(nspname + 14); else diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb811520c29..76406b5a9d9 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -1623,7 +1623,8 @@ RemoveRelations(DropStmt *drop) state.heapOid = InvalidOid; state.partParentOid = InvalidOid; - relOid = RangeVarGetRelidExtended(rel, lockmode, RVR_MISSING_OK, + relOid = RangeVarGetRelidExtended(rel, lockmode, + RVR_MISSING_OK | RVR_OTHER_TEMP_OK, RangeVarCallbackForDropRelation, &state); diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index e2d9e9be41a..62edf24b5c2 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -478,10 +478,14 @@ makeRangeVar(char *schemaname, char *relname, int location) r->schemaname = schemaname; r->relname = relname; r->inh = true; - r->relpersistence = RELPERSISTENCE_PERMANENT; r->alias = NULL; r->location = location; + if (r->schemaname && strncmp(r->schemaname, "pg_temp", 7) == 0) + r->relpersistence = RELPERSISTENCE_TEMP; + else + r->relpersistence = RELPERSISTENCE_PERMANENT; + return r; } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index db43034b9db..1b959e752c3 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -19405,7 +19405,11 @@ makeRangeVarFromAnyName(List *names, int position, core_yyscan_t yyscanner) break; } - r->relpersistence = RELPERSISTENCE_PERMANENT; + if (r->schemaname && strncmp(r->schemaname, "pg_temp", 7) == 0) + r->relpersistence = RELPERSISTENCE_TEMP; + else + r->relpersistence = RELPERSISTENCE_PERMANENT; + r->location = position; return r; @@ -19445,6 +19449,11 @@ makeRangeVarFromQualifiedName(char *name, List *namelist, int location, break; } + if (r->schemaname && strncmp(r->schemaname, "pg_temp", 7) == 0) + r->relpersistence = RELPERSISTENCE_TEMP; + else + r->relpersistence = RELPERSISTENCE_PERMANENT; + return r; } diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..9c45a30516e 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -72,6 +72,8 @@ typedef enum RVROption RVR_MISSING_OK = 1 << 0, /* don't error if relation doesn't exist */ RVR_NOWAIT = 1 << 1, /* error if relation cannot be locked */ RVR_SKIP_LOCKED = 1 << 2, /* skip if relation cannot be locked */ + RVR_OTHER_TEMP_OK = 1 << 3 /* don't error if relation is temp relation of + other session (needed for DROP command) */ } RVROption; typedef void (*RangeVarGetRelidCallback) (const RangeVar *relation, Oid relId, -- 2.48.1
From c0cbdf310752b51e2c4753bd1e81abfd83c759be Mon Sep 17 00:00:00 2001 From: Stepan Neretin <slp...@gmail.com> Date: Mon, 28 Jul 2025 10:24:20 +0700 Subject: [PATCH v6 2/2] Prevent cross-session temp table access test Adds an isolation test verifying that temp tables created in one session cannot be accessed from another session using the temp schema name. Session 1 creates a temp table and records its temp schema name in a regular table. Session 2 attempts to query that temp table via the stored schema name, expecting an error. --- src/test/isolation/expected/temp-access.out | 32 +++++++++++++++++++++ src/test/isolation/isolation_schedule | 1 + src/test/isolation/specs/temp-access.spec | 32 +++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/test/isolation/expected/temp-access.out create mode 100644 src/test/isolation/specs/temp-access.spec diff --git a/src/test/isolation/expected/temp-access.out b/src/test/isolation/expected/temp-access.out new file mode 100644 index 00000000000..d402add4d83 --- /dev/null +++ b/src/test/isolation/expected/temp-access.out @@ -0,0 +1,32 @@ +Parsed test spec with 2 sessions + +starting permutation: create_temp_table wait try_access_temp_table +step create_temp_table: + CREATE TEMP TABLE temp_table1(a int); + CREATE TABLE IF NOT EXISTS temp_schema_name(schema_name text); + TRUNCATE temp_schema_name; + INSERT INTO temp_schema_name + SELECT n.nspname + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relname = 'temp_table1'; + +step wait: + SELECT 1; + +?column? +-------- + 1 +(1 row) + +step try_access_temp_table: + DO $$ + DECLARE + nspname TEXT; + BEGIN + SELECT schema_name INTO nspname FROM temp_schema_name LIMIT 1; + EXECUTE format('SELECT * FROM %I.temp_table1', nspname); + END + $$; + +ERROR: could not access temporary relations of other sessions diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..e2d3d330fd0 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -45,6 +45,7 @@ test: lock-update-delete test: lock-update-traversal test: inherit-temp test: temp-schema-cleanup +test: temp-access test: insert-conflict-do-nothing test: insert-conflict-do-nothing-2 test: insert-conflict-do-update diff --git a/src/test/isolation/specs/temp-access.spec b/src/test/isolation/specs/temp-access.spec new file mode 100644 index 00000000000..35383fe7b35 --- /dev/null +++ b/src/test/isolation/specs/temp-access.spec @@ -0,0 +1,32 @@ +session s1 +step create_temp_table +{ + CREATE TEMP TABLE temp_table1(a int); + CREATE TABLE IF NOT EXISTS temp_schema_name(schema_name text); + TRUNCATE temp_schema_name; + INSERT INTO temp_schema_name + SELECT n.nspname + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relname = 'temp_table1'; +} + +session s2 +step wait +{ + SELECT 1; +} + +step try_access_temp_table +{ + DO $$ + DECLARE + nspname TEXT; + BEGIN + SELECT schema_name INTO nspname FROM temp_schema_name LIMIT 1; + EXECUTE format('SELECT * FROM %I.temp_table1', nspname); + END + $$; +} + +permutation create_temp_table wait try_access_temp_table -- 2.48.1