Re: Request for assistance to backport CVE-2022-1552 fixes to 9.6 and 9.4
От | Roberto C. Sánchez |
---|---|
Тема | Re: Request for assistance to backport CVE-2022-1552 fixes to 9.6 and 9.4 |
Дата | |
Msg-id | YuFfT9Dyp1aNbBKP@connexer.com обсуждение исходный текст |
Ответ на | Re: Request for assistance to backport CVE-2022-1552 fixes to 9.6 and 9.4 (Roberto C. Sánchez <roberto@debian.org>) |
Список | pgsql-hackers |
Hello pgsql-hackers, Is there anyone willing to review the patches that I prepared? I'd have substatntially more confidence in the patches with a review from an upstream developer who is familiar with the code. Regards, -Roberto On Mon, Jul 04, 2022 at 06:06:58PM -0400, Roberto C. Sánchez wrote: > On Wed, Jun 08, 2022 at 05:31:22PM -0400, Roberto C. Sánchez wrote: > > On Wed, Jun 08, 2022 at 04:15:47PM -0400, Tom Lane wrote: > > > Roberto =?iso-8859-1?Q?C=2E_S=E1nchez?= <roberto@debian.org> writes: > > > > I am investigating backporting the fixes for CVE-2022-1552 to 9.6 and > > > > 9.4 as part of Debian LTS and Extended LTS. I am aware that these > > > > releases are no longer supported upstream, but I have made an attempt at > > > > adapting commits ef792f7856dea2576dcd9cab92b2b05fe955696b and > > > > f26d5702857a9c027f84850af48b0eea0f3aa15c from the REL_10_STABLE branch. > > > > I would appreciate a review of the attached patches and any comments on > > > > things that may have been missed and/or adapted improperly. > > > > > > FWIW, I would not recommend being in a huge hurry to back-port those > > > changes, pending the outcome of this discussion: > > > > > > https://www.postgresql.org/message-id/flat/f8a4105f076544c180a87ef0c4822352%40stmuk.bayern.de > > > > > Thanks for the pointer. > > > > > We're going to have to tweak that code somehow, and it's not yet > > > entirely clear how. > > > > > I will monitor the discussion to see what comes of it. > > > Based on the discussion in the other thread, I have made an attempt to > backport commit 88b39e61486a8925a3861d50c306a51eaa1af8d6 to 9.6 and 9.4. > The only significant change that I had to make was to add the full > function signatures for the REVOKE/GRANT in the citext test. > > One question that I had about the change as committed is whether a > REVOKE is needed on s.citext_ne, like so: > > REVOKE ALL ON FUNCTION s.citext_ne FROM PUBLIC; > > Or (for pre-10): > > REVOKE ALL ON FUNCTION s.citext_ne(s.citext, s.citext) FROM PUBLIC; > > I ask because the comment immediately preceding the sequence of REVOKEs > includes the comment "Revoke all conceivably-relevant ACLs within the > extension." I am not especially knowledgable about deep internals, but > that function seems like it would belong in the same group with the > others. > > In any event, would someone be willing to review the attached patches > for correctness? I would like to shortly publish updates to 9.6 and 9.4 > in Debian and a review would be most appreciated. > > Regards, > > -Roberto > > -- > Roberto C. Sánchez > From ef792f7856dea2576dcd9cab92b2b05fe955696b Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Mon, 9 May 2022 08:35:08 -0700 > Subject: [PATCH] Make relation-enumerating operations be security-restricted > operations. > > When a feature enumerates relations and runs functions associated with > all found relations, the feature's user shall not need to trust every > user having permission to create objects. BRIN-specific functionality > in autovacuum neglected to account for this, as did pg_amcheck and > CLUSTER. An attacker having permission to create non-temp objects in at > least one schema could execute arbitrary SQL functions under the > identity of the bootstrap superuser. CREATE INDEX (not a > relation-enumerating operation) and REINDEX protected themselves too > late. This change extends to the non-enumerating amcheck interface. > Back-patch to v10 (all supported versions). > > Sergey Shinderuk, reviewed (in earlier versions) by Alexander Lakhin. > Reported by Alexander Lakhin. > > Security: CVE-2022-1552 > --- > src/backend/access/brin/brin.c | 30 ++++++++++++++++- > src/backend/catalog/index.c | 41 +++++++++++++++++------ > src/backend/commands/cluster.c | 35 ++++++++++++++++---- > src/backend/commands/indexcmds.c | 53 +++++++++++++++++++++++++++++-- > src/backend/utils/init/miscinit.c | 24 ++++++++------ > src/test/regress/expected/privileges.out | 42 ++++++++++++++++++++++++ > src/test/regress/sql/privileges.sql | 36 +++++++++++++++++++++ > 7 files changed, 231 insertions(+), 30 deletions(-) > > --- a/src/backend/access/brin/brin.c > +++ b/src/backend/access/brin/brin.c > @@ -28,6 +28,7 @@ > #include "pgstat.h" > #include "storage/bufmgr.h" > #include "storage/freespace.h" > +#include "utils/guc.h" > #include "utils/index_selfuncs.h" > #include "utils/memutils.h" > #include "utils/rel.h" > @@ -786,6 +787,9 @@ > Oid heapoid; > Relation indexRel; > Relation heapRel; > + Oid save_userid; > + int save_sec_context; > + int save_nestlevel; > double numSummarized = 0; > > if (RecoveryInProgress()) > @@ -799,10 +803,28 @@ > * passed indexoid isn't an index then IndexGetRelation() will fail. > * Rather than emitting a not-very-helpful error message, postpone > * complaining, expecting that the is-it-an-index test below will fail. > + * > + * unlike brin_summarize_range(), autovacuum never calls this. hence, we > + * don't switch userid. > */ > heapoid = IndexGetRelation(indexoid, true); > if (OidIsValid(heapoid)) > + { > heapRel = heap_open(heapoid, ShareUpdateExclusiveLock); > + > + /* > + * Autovacuum calls us. For its benefit, switch to the table owner's > + * userid, so that any index functions are run as that user. Also > + * lock down security-restricted operations and arrange to make GUC > + * variable changes local to this command. This is harmless, albeit > + * unnecessary, when called from SQL, because we fail shortly if the > + * user does not own the index. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(heapRel->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + } > else > heapRel = NULL; > > @@ -817,7 +839,7 @@ > RelationGetRelationName(indexRel)))); > > /* User must own the index (comparable to privileges needed for VACUUM) */ > - if (!pg_class_ownercheck(indexoid, GetUserId())) > + if (heapRel != NULL && !pg_class_ownercheck(indexoid, save_userid)) > aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, > RelationGetRelationName(indexRel)); > > @@ -835,6 +857,12 @@ > /* OK, do it */ > brinsummarize(indexRel, heapRel, &numSummarized, NULL); > > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(save_userid, save_sec_context); > + > relation_close(indexRel, ShareUpdateExclusiveLock); > relation_close(heapRel, ShareUpdateExclusiveLock); > > --- a/src/backend/catalog/index.c > +++ b/src/backend/catalog/index.c > @@ -2908,7 +2908,17 @@ > > /* Open and lock the parent heap relation */ > heapRelation = heap_open(heapId, ShareUpdateExclusiveLock); > - /* And the target index relation */ > + > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > indexRelation = index_open(indexId, RowExclusiveLock); > > /* > @@ -2922,16 +2932,6 @@ > indexInfo->ii_Concurrent = true; > > /* > - * Switch to the table owner's userid, so that any index functions are run > - * as that user. Also lock down security-restricted operations and > - * arrange to make GUC variable changes local to this command. > - */ > - GetUserIdAndSecContext(&save_userid, &save_sec_context); > - SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > - save_sec_context | SECURITY_RESTRICTED_OPERATION); > - save_nestlevel = NewGUCNestLevel(); > - > - /* > * Scan the index and gather up all the TIDs into a tuplesort object. > */ > ivinfo.index = indexRelation; > @@ -3395,6 +3395,9 @@ > Relation iRel, > heapRelation; > Oid heapId; > + Oid save_userid; > + int save_sec_context; > + int save_nestlevel; > IndexInfo *indexInfo; > volatile bool skipped_constraint = false; > PGRUsage ru0; > @@ -3409,6 +3412,16 @@ > heapRelation = heap_open(heapId, ShareLock); > > /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > + /* > * Open the target index relation and get an exclusive lock on it, to > * ensure that no one else is touching this particular index. > */ > @@ -3550,6 +3563,12 @@ > errdetail_internal("%s", > pg_rusage_show(&ru0)))); > > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(save_userid, save_sec_context); > + > /* Close rels, but keep locks */ > index_close(iRel, NoLock); > heap_close(heapRelation, NoLock); > --- a/src/backend/commands/cluster.c > +++ b/src/backend/commands/cluster.c > @@ -44,6 +44,7 @@ > #include "storage/smgr.h" > #include "utils/acl.h" > #include "utils/fmgroids.h" > +#include "utils/guc.h" > #include "utils/inval.h" > #include "utils/lsyscache.h" > #include "utils/memutils.h" > @@ -260,6 +261,9 @@ > cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose) > { > Relation OldHeap; > + Oid save_userid; > + int save_sec_context; > + int save_nestlevel; > > /* Check for user-requested abort. */ > CHECK_FOR_INTERRUPTS(); > @@ -277,6 +281,16 @@ > return; > > /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(OldHeap->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > + /* > * Since we may open a new transaction for each relation, we have to check > * that the relation still is what we think it is. > * > @@ -290,10 +304,10 @@ > Form_pg_index indexForm; > > /* Check that the user still owns the relation */ > - if (!pg_class_ownercheck(tableOid, GetUserId())) > + if (!pg_class_ownercheck(tableOid, save_userid)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -307,7 +321,7 @@ > if (RELATION_IS_OTHER_TEMP(OldHeap)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > if (OidIsValid(indexOid)) > @@ -318,7 +332,7 @@ > if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -328,14 +342,14 @@ > if (!HeapTupleIsValid(tuple)) /* probably can't happen */ > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > indexForm = (Form_pg_index) GETSTRUCT(tuple); > if (!indexForm->indisclustered) > { > ReleaseSysCache(tuple); > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > ReleaseSysCache(tuple); > } > @@ -389,7 +403,7 @@ > !RelationIsPopulated(OldHeap)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -404,6 +418,13 @@ > rebuild_relation(OldHeap, indexOid, verbose); > > /* NB: rebuild_relation does heap_close() on OldHeap */ > + > +out: > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(save_userid, save_sec_context); > } > > /* > --- a/src/backend/commands/indexcmds.c > +++ b/src/backend/commands/indexcmds.c > @@ -49,6 +49,7 @@ > #include "utils/acl.h" > #include "utils/builtins.h" > #include "utils/fmgroids.h" > +#include "utils/guc.h" > #include "utils/inval.h" > #include "utils/lsyscache.h" > #include "utils/memutils.h" > @@ -339,8 +340,13 @@ > LOCKTAG heaplocktag; > LOCKMODE lockmode; > Snapshot snapshot; > + Oid root_save_userid; > + int root_save_sec_context; > + int root_save_nestlevel; > int i; > > + root_save_nestlevel = NewGUCNestLevel(); > + > /* > * Force non-concurrent build on temporary relations, even if CONCURRENTLY > * was requested. Other backends can't access a temporary relation, so > @@ -381,6 +387,15 @@ > lockmode = concurrent ? ShareUpdateExclusiveLock : ShareLock; > rel = heap_open(relationId, lockmode); > > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations. We > + * already arranged to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&root_save_userid, &root_save_sec_context); > + SetUserIdAndSecContext(rel->rd_rel->relowner, > + root_save_sec_context | SECURITY_RESTRICTED_OPERATION); > + > relationId = RelationGetRelid(rel); > namespaceId = RelationGetNamespace(rel); > > @@ -422,7 +437,7 @@ > { > AclResult aclresult; > > - aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), > + aclresult = pg_namespace_aclcheck(namespaceId, root_save_userid, > ACL_CREATE); > if (aclresult != ACLCHECK_OK) > aclcheck_error(aclresult, ACL_KIND_NAMESPACE, > @@ -449,7 +464,7 @@ > { > AclResult aclresult; > > - aclresult = pg_tablespace_aclcheck(tablespaceId, GetUserId(), > + aclresult = pg_tablespace_aclcheck(tablespaceId, root_save_userid, > ACL_CREATE); > if (aclresult != ACLCHECK_OK) > aclcheck_error(aclresult, ACL_KIND_TABLESPACE, > @@ -679,15 +694,33 @@ > > if (!OidIsValid(indexRelationId)) > { > + /* Roll back any GUC changes executed by index functions. */ > + AtEOXact_GUC(false, root_save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(root_save_userid, root_save_sec_context); > + > heap_close(rel, NoLock); > return address; > } > > + /* > + * Roll back any GUC changes executed by index functions, and keep > + * subsequent changes local to this command. It's barely possible that > + * some index function changed a behavior-affecting GUC, e.g. xmloption, > + * that affects subsequent steps. > + */ > + AtEOXact_GUC(false, root_save_nestlevel); > + root_save_nestlevel = NewGUCNestLevel(); > + > /* Add any requested comment */ > if (stmt->idxcomment != NULL) > CreateComments(indexRelationId, RelationRelationId, 0, > stmt->idxcomment); > > + AtEOXact_GUC(false, root_save_nestlevel); > + SetUserIdAndSecContext(root_save_userid, root_save_sec_context); > + > if (!concurrent) > { > /* Close the heap and we're done, in the non-concurrent case */ > @@ -766,6 +799,16 @@ > /* Open and lock the parent heap relation */ > rel = heap_openrv(stmt->relation, ShareUpdateExclusiveLock); > > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&root_save_userid, &root_save_sec_context); > + SetUserIdAndSecContext(rel->rd_rel->relowner, > + root_save_sec_context | SECURITY_RESTRICTED_OPERATION); > + root_save_nestlevel = NewGUCNestLevel(); > + > /* And the target index relation */ > indexRelation = index_open(indexRelationId, RowExclusiveLock); > > @@ -781,6 +824,12 @@ > /* Now build the index */ > index_build(rel, indexRelation, indexInfo, stmt->primary, false); > > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, root_save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(root_save_userid, root_save_sec_context); > + > /* Close both the relations, but keep the locks */ > heap_close(rel, NoLock); > index_close(indexRelation, NoLock); > --- a/src/backend/utils/init/miscinit.c > +++ b/src/backend/utils/init/miscinit.c > @@ -365,15 +365,21 @@ > * with guc.c's internal state, so SET ROLE has to be disallowed. > * > * SECURITY_RESTRICTED_OPERATION indicates that we are inside an operation > - * that does not wish to trust called user-defined functions at all. This > - * bit prevents not only SET ROLE, but various other changes of session state > - * that normally is unprotected but might possibly be used to subvert the > - * calling session later. An example is replacing an existing prepared > - * statement with new code, which will then be executed with the outer > - * session's permissions when the prepared statement is next used. Since > - * these restrictions are fairly draconian, we apply them only in contexts > - * where the called functions are really supposed to be side-effect-free > - * anyway, such as VACUUM/ANALYZE/REINDEX. > + * that does not wish to trust called user-defined functions at all. The > + * policy is to use this before operations, e.g. autovacuum and REINDEX, that > + * enumerate relations of a database or schema and run functions associated > + * with each found relation. The relation owner is the new user ID. Set this > + * as soon as possible after locking the relation. Restore the old user ID as > + * late as possible before closing the relation; restoring it shortly after > + * close is also tolerable. If a command has both relation-enumerating and > + * non-enumerating modes, e.g. ANALYZE, both modes set this bit. This bit > + * prevents not only SET ROLE, but various other changes of session state that > + * normally is unprotected but might possibly be used to subvert the calling > + * session later. An example is replacing an existing prepared statement with > + * new code, which will then be executed with the outer session's permissions > + * when the prepared statement is next used. These restrictions are fairly > + * draconian, but the functions called in relation-enumerating operations are > + * really supposed to be side-effect-free anyway. > * > * SECURITY_NOFORCE_RLS indicates that we are inside an operation which should > * ignore the FORCE ROW LEVEL SECURITY per-table indication. This is used to > --- a/src/test/regress/expected/privileges.out > +++ b/src/test/regress/expected/privileges.out > @@ -1244,6 +1244,48 @@ > -- security-restricted operations > \c - > CREATE ROLE regress_sro_user; > +-- Check that index expressions and predicates are run as the table's owner > +-- A dummy index function checking current_user > +CREATE FUNCTION sro_ifun(int) RETURNS int AS $$ > +BEGIN > + -- Below we set the table's owner to regress_sro_user > + ASSERT current_user = 'regress_sro_user', > + format('sro_ifun(%s) called by %s', $1, current_user); > + RETURN $1; > +END; > +$$ LANGUAGE plpgsql IMMUTABLE; > +-- Create a table owned by regress_sro_user > +CREATE TABLE sro_tab (a int); > +ALTER TABLE sro_tab OWNER TO regress_sro_user; > +INSERT INTO sro_tab VALUES (1), (2), (3); > +-- Create an expression index with a predicate > +CREATE INDEX sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +DROP INDEX sro_idx; > +-- Do the same concurrently > +CREATE INDEX CONCURRENTLY sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +-- REINDEX > +REINDEX TABLE sro_tab; > +REINDEX INDEX sro_idx; > +REINDEX TABLE CONCURRENTLY sro_tab; -- v12+ feature > +ERROR: syntax error at or near "CONCURRENTLY" > +LINE 1: REINDEX TABLE CONCURRENTLY sro_tab; > + ^ > +DROP INDEX sro_idx; > +-- CLUSTER > +CREATE INDEX sro_cluster_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))); > +CLUSTER sro_tab USING sro_cluster_idx; > +DROP INDEX sro_cluster_idx; > +-- BRIN index > +CREATE INDEX sro_brin ON sro_tab USING brin ((sro_ifun(a) + sro_ifun(0))); > +SELECT brin_summarize_new_values('sro_brin'); > + brin_summarize_new_values > +--------------------------- > + 0 > +(1 row) > + > +DROP TABLE sro_tab; > SET SESSION AUTHORIZATION regress_sro_user; > CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS > 'GRANT regress_group2 TO regress_sro_user'; > --- a/src/test/regress/sql/privileges.sql > +++ b/src/test/regress/sql/privileges.sql > @@ -762,6 +762,42 @@ > \c - > CREATE ROLE regress_sro_user; > > +-- Check that index expressions and predicates are run as the table's owner > + > +-- A dummy index function checking current_user > +CREATE FUNCTION sro_ifun(int) RETURNS int AS $$ > +BEGIN > + -- Below we set the table's owner to regress_sro_user > + ASSERT current_user = 'regress_sro_user', > + format('sro_ifun(%s) called by %s', $1, current_user); > + RETURN $1; > +END; > +$$ LANGUAGE plpgsql IMMUTABLE; > +-- Create a table owned by regress_sro_user > +CREATE TABLE sro_tab (a int); > +ALTER TABLE sro_tab OWNER TO regress_sro_user; > +INSERT INTO sro_tab VALUES (1), (2), (3); > +-- Create an expression index with a predicate > +CREATE INDEX sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +DROP INDEX sro_idx; > +-- Do the same concurrently > +CREATE INDEX CONCURRENTLY sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +-- REINDEX > +REINDEX TABLE sro_tab; > +REINDEX INDEX sro_idx; > +REINDEX TABLE CONCURRENTLY sro_tab; -- v12+ feature > +DROP INDEX sro_idx; > +-- CLUSTER > +CREATE INDEX sro_cluster_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))); > +CLUSTER sro_tab USING sro_cluster_idx; > +DROP INDEX sro_cluster_idx; > +-- BRIN index > +CREATE INDEX sro_brin ON sro_tab USING brin ((sro_ifun(a) + sro_ifun(0))); > +SELECT brin_summarize_new_values('sro_brin'); > +DROP TABLE sro_tab; > + > SET SESSION AUTHORIZATION regress_sro_user; > CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS > 'GRANT regress_group2 TO regress_sro_user'; > From f26d5702857a9c027f84850af48b0eea0f3aa15c Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Mon, 9 May 2022 08:35:08 -0700 > Subject: [PATCH] In REFRESH MATERIALIZED VIEW, set user ID before running user > code. > > It intended to, but did not, achieve this. Adopt the new standard of > setting user ID just after locking the relation. Back-patch to v10 (all > supported versions). > > Reviewed by Simon Riggs. Reported by Alvaro Herrera. > > Security: CVE-2022-1552 > --- > src/backend/commands/matview.c | 30 +++++++++++------------------- > src/test/regress/expected/privileges.out | 15 +++++++++++++++ > src/test/regress/sql/privileges.sql | 16 ++++++++++++++++ > 3 files changed, 42 insertions(+), 19 deletions(-) > > --- a/src/backend/commands/matview.c > +++ b/src/backend/commands/matview.c > @@ -164,6 +164,17 @@ > lockmode, false, false, > RangeVarCallbackOwnsTable, NULL); > matviewRel = heap_open(matviewOid, NoLock); > + relowner = matviewRel->rd_rel->relowner; > + > + /* > + * Switch to the owner's userid, so that any functions are run as that > + * user. Also lock down security-restricted operations and arrange to > + * make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > > /* Make sure it is a materialized view. */ > if (matviewRel->rd_rel->relkind != RELKIND_MATVIEW) > @@ -269,19 +280,6 @@ > */ > SetMatViewPopulatedState(matviewRel, !stmt->skipData); > > - relowner = matviewRel->rd_rel->relowner; > - > - /* > - * Switch to the owner's userid, so that any functions are run as that > - * user. Also arrange to make GUC variable changes local to this command. > - * Don't lock it down too tight to create a temporary table just yet. We > - * will switch modes when we are about to execute user code. > - */ > - GetUserIdAndSecContext(&save_userid, &save_sec_context); > - SetUserIdAndSecContext(relowner, > - save_sec_context | SECURITY_LOCAL_USERID_CHANGE); > - save_nestlevel = NewGUCNestLevel(); > - > /* Concurrent refresh builds new data in temp tablespace, and does diff. */ > if (concurrent) > { > @@ -304,12 +302,6 @@ > LockRelationOid(OIDNewHeap, AccessExclusiveLock); > dest = CreateTransientRelDestReceiver(OIDNewHeap); > > - /* > - * Now lock down security-restricted operations. > - */ > - SetUserIdAndSecContext(relowner, > - save_sec_context | SECURITY_RESTRICTED_OPERATION); > - > /* Generate the data, if wanted. */ > if (!stmt->skipData) > refresh_matview_datafill(dest, dataQuery, queryString); > --- a/src/test/regress/expected/privileges.out > +++ b/src/test/regress/expected/privileges.out > @@ -1323,6 +1323,21 @@ > SQL statement "SELECT unwanted_grant()" > PL/pgSQL function sro_trojan() line 1 at PERFORM > SQL function "mv_action" statement 1 > +-- REFRESH MATERIALIZED VIEW CONCURRENTLY use of eval_const_expressions() > +SET SESSION AUTHORIZATION regress_sro_user; > +CREATE FUNCTION unwanted_grant_nofail(int) RETURNS int > + IMMUTABLE LANGUAGE plpgsql AS $$ > +BEGIN > + PERFORM unwanted_grant(); > + RAISE WARNING 'owned'; > + RETURN 1; > +EXCEPTION WHEN OTHERS THEN > + RETURN 2; > +END$$; > +CREATE MATERIALIZED VIEW sro_index_mv AS SELECT 1 AS c; > +CREATE UNIQUE INDEX ON sro_index_mv (c) WHERE unwanted_grant_nofail(1) > 0; > +\c - > +REFRESH MATERIALIZED VIEW sro_index_mv; > DROP OWNED BY regress_sro_user; > DROP ROLE regress_sro_user; > -- Admin options > --- a/src/test/regress/sql/privileges.sql > +++ b/src/test/regress/sql/privileges.sql > @@ -824,6 +824,22 @@ > REFRESH MATERIALIZED VIEW sro_mv; > BEGIN; SET CONSTRAINTS ALL IMMEDIATE; REFRESH MATERIALIZED VIEW sro_mv; COMMIT; > > +-- REFRESH MATERIALIZED VIEW CONCURRENTLY use of eval_const_expressions() > +SET SESSION AUTHORIZATION regress_sro_user; > +CREATE FUNCTION unwanted_grant_nofail(int) RETURNS int > + IMMUTABLE LANGUAGE plpgsql AS $$ > +BEGIN > + PERFORM unwanted_grant(); > + RAISE WARNING 'owned'; > + RETURN 1; > +EXCEPTION WHEN OTHERS THEN > + RETURN 2; > +END$$; > +CREATE MATERIALIZED VIEW sro_index_mv AS SELECT 1 AS c; > +CREATE UNIQUE INDEX ON sro_index_mv (c) WHERE unwanted_grant_nofail(1) > 0; > +\c - > +REFRESH MATERIALIZED VIEW sro_index_mv; > + > DROP OWNED BY regress_sro_user; > DROP ROLE regress_sro_user; > > From 88b39e61486a8925a3861d50c306a51eaa1af8d6 Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Sat, 25 Jun 2022 09:07:41 -0700 > Subject: [PATCH] CREATE INDEX: use the original userid for more ACL checks. > > Commit a117cebd638dd02e5c2e791c25e43745f233111b used the original userid > for ACL checks located directly in DefineIndex(), but it still adopted > the table owner userid for more ACL checks than intended. That broke > dump/reload of indexes that refer to an operator class, collation, or > exclusion operator in a schema other than "public" or "pg_catalog". > Back-patch to v10 (all supported versions), like the earlier commit. > > Nathan Bossart and Noah Misch > > Discussion: https://postgr.es/m/f8a4105f076544c180a87ef0c4822352@stmuk.bayern.de > --- > contrib/citext/Makefile | 2 > contrib/citext/expected/create_index_acl.out | 81 +++++++++++++++++++++++ > contrib/citext/sql/create_index_acl.sql | 82 ++++++++++++++++++++++++ > src/backend/commands/indexcmds.c | 92 +++++++++++++++++++++++---- > 4 files changed, 244 insertions(+), 13 deletions(-) > create mode 100644 contrib/citext/expected/create_index_acl.out > create mode 100644 contrib/citext/sql/create_index_acl.sql > > --- a/contrib/citext/Makefile > +++ b/contrib/citext/Makefile > @@ -7,7 +7,7 @@ > citext--1.0--1.1.sql citext--unpackaged--1.0.sql > PGFILEDESC = "citext - case-insensitive character string data type" > > -REGRESS = citext > +REGRESS = create_index_acl citext > > ifdef USE_PGXS > PG_CONFIG = pg_config > --- /dev/null > +++ b/contrib/citext/expected/create_index_acl.out > @@ -0,0 +1,81 @@ > +-- Each DefineIndex() ACL check uses either the original userid or the table > +-- owner userid; see its header comment. Here, confirm that DefineIndex() > +-- uses its original userid where necessary. The test works by creating > +-- indexes that refer to as many sorts of objects as possible, with the table > +-- owner having as few applicable privileges as possible. (The privileges.sql > +-- regress_sro_user tests look for the opposite defect; they confirm that > +-- DefineIndex() uses the table owner userid where necessary.) > +-- Don't override tablespaces; this version lacks allow_in_place_tablespaces. > +BEGIN; > +CREATE ROLE regress_minimal; > +CREATE SCHEMA s; > +CREATE EXTENSION citext SCHEMA s; > +-- Revoke all conceivably-relevant ACLs within the extension. The system > +-- doesn't check all these ACLs, but this will provide some coverage if that > +-- ever changes. > +REVOKE ALL ON TYPE s.citext FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_lt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_le(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_eq(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_ge(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_gt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_cmp(s.citext, s.citext) FROM PUBLIC; > +-- Functions sufficient for making an index column that has the side effect of > +-- changing search_path at expression planning time. > +CREATE FUNCTION public.setter() RETURNS bool VOLATILE > + LANGUAGE SQL AS $$SET search_path = s; SELECT true$$; > +CREATE FUNCTION s.const() RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT public.setter()$$; > +CREATE FUNCTION s.index_this_expr(s.citext, bool) RETURNS s.citext IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1$$; > +REVOKE ALL ON FUNCTION public.setter() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.const() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.index_this_expr(s.citext, bool) FROM PUBLIC; > +-- Even for an empty table, expression planning calls s.const & public.setter. > +GRANT EXECUTE ON FUNCTION public.setter() TO regress_minimal; > +GRANT EXECUTE ON FUNCTION s.const() TO regress_minimal; > +-- Function for index predicate. > +CREATE FUNCTION s.index_row_if(s.citext) RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1 IS NOT NULL$$; > +REVOKE ALL ON FUNCTION s.index_row_if(s.citext) FROM PUBLIC; > +-- Even for an empty table, CREATE INDEX checks ii_Predicate permissions. > +GRANT EXECUTE ON FUNCTION s.index_row_if(s.citext) TO regress_minimal; > +-- Non-extension, non-function objects. > +CREATE COLLATION s.coll (LOCALE="C"); > +CREATE TABLE s.x (y s.citext); > +ALTER TABLE s.x OWNER TO regress_minimal; > +-- Empty-table DefineIndex() > +CREATE UNIQUE INDEX u0rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e0rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Make the table nonempty. > +INSERT INTO s.x VALUES ('foo'), ('bar'); > +-- If the INSERT runs the planner on index expressions, a search_path change > +-- survives. As of 2022-06, the INSERT reuses a cached plan. It does so even > +-- under debug_discard_caches, since each index is new-in-transaction. If > +-- future work changes a cache lifecycle, this RESET may become necessary. > +RESET search_path; > +-- For a nonempty table, owner needs permissions throughout ii_Expressions. > +GRANT EXECUTE ON FUNCTION s.index_this_expr(s.citext, bool) TO regress_minimal; > +CREATE UNIQUE INDEX u2rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e2rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Shall not find s.coll via search_path, despite the s.const->public.setter > +-- call having set search_path=s during expression planning. Suppress the > +-- message itself, which depends on the database encoding. > +\set VERBOSITY terse > +DO $$ > +BEGIN > +ALTER TABLE s.x ADD CONSTRAINT underqualified EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE coll WITH s.=) > + WHERE (s.index_row_if(y)); > +EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION '%', sqlstate; END$$; > +ERROR: 42704 > +\set VERBOSITY default > +ROLLBACK; > --- /dev/null > +++ b/contrib/citext/sql/create_index_acl.sql > @@ -0,0 +1,82 @@ > +-- Each DefineIndex() ACL check uses either the original userid or the table > +-- owner userid; see its header comment. Here, confirm that DefineIndex() > +-- uses its original userid where necessary. The test works by creating > +-- indexes that refer to as many sorts of objects as possible, with the table > +-- owner having as few applicable privileges as possible. (The privileges.sql > +-- regress_sro_user tests look for the opposite defect; they confirm that > +-- DefineIndex() uses the table owner userid where necessary.) > + > +-- Don't override tablespaces; this version lacks allow_in_place_tablespaces. > + > +BEGIN; > +CREATE ROLE regress_minimal; > +CREATE SCHEMA s; > +CREATE EXTENSION citext SCHEMA s; > +-- Revoke all conceivably-relevant ACLs within the extension. The system > +-- doesn't check all these ACLs, but this will provide some coverage if that > +-- ever changes. > +REVOKE ALL ON TYPE s.citext FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_lt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_le(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_eq(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_ge(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_gt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_cmp(s.citext, s.citext) FROM PUBLIC; > +-- Functions sufficient for making an index column that has the side effect of > +-- changing search_path at expression planning time. > +CREATE FUNCTION public.setter() RETURNS bool VOLATILE > + LANGUAGE SQL AS $$SET search_path = s; SELECT true$$; > +CREATE FUNCTION s.const() RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT public.setter()$$; > +CREATE FUNCTION s.index_this_expr(s.citext, bool) RETURNS s.citext IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1$$; > +REVOKE ALL ON FUNCTION public.setter() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.const() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.index_this_expr(s.citext, bool) FROM PUBLIC; > +-- Even for an empty table, expression planning calls s.const & public.setter. > +GRANT EXECUTE ON FUNCTION public.setter() TO regress_minimal; > +GRANT EXECUTE ON FUNCTION s.const() TO regress_minimal; > +-- Function for index predicate. > +CREATE FUNCTION s.index_row_if(s.citext) RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1 IS NOT NULL$$; > +REVOKE ALL ON FUNCTION s.index_row_if(s.citext) FROM PUBLIC; > +-- Even for an empty table, CREATE INDEX checks ii_Predicate permissions. > +GRANT EXECUTE ON FUNCTION s.index_row_if(s.citext) TO regress_minimal; > +-- Non-extension, non-function objects. > +CREATE COLLATION s.coll (LOCALE="C"); > +CREATE TABLE s.x (y s.citext); > +ALTER TABLE s.x OWNER TO regress_minimal; > +-- Empty-table DefineIndex() > +CREATE UNIQUE INDEX u0rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e0rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Make the table nonempty. > +INSERT INTO s.x VALUES ('foo'), ('bar'); > +-- If the INSERT runs the planner on index expressions, a search_path change > +-- survives. As of 2022-06, the INSERT reuses a cached plan. It does so even > +-- under debug_discard_caches, since each index is new-in-transaction. If > +-- future work changes a cache lifecycle, this RESET may become necessary. > +RESET search_path; > +-- For a nonempty table, owner needs permissions throughout ii_Expressions. > +GRANT EXECUTE ON FUNCTION s.index_this_expr(s.citext, bool) TO regress_minimal; > +CREATE UNIQUE INDEX u2rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e2rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Shall not find s.coll via search_path, despite the s.const->public.setter > +-- call having set search_path=s during expression planning. Suppress the > +-- message itself, which depends on the database encoding. > +\set VERBOSITY terse > +DO $$ > +BEGIN > +ALTER TABLE s.x ADD CONSTRAINT underqualified EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE coll WITH s.=) > + WHERE (s.index_row_if(y)); > +EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION '%', sqlstate; END$$; > +\set VERBOSITY default > +ROLLBACK; > --- a/src/backend/commands/indexcmds.c > +++ b/src/backend/commands/indexcmds.c > @@ -70,7 +70,10 @@ > Oid relId, > char *accessMethodName, Oid accessMethodId, > bool amcanorder, > - bool isconstraint); > + bool isconstraint, > + Oid ddl_userid, > + int ddl_sec_context, > + int *ddl_save_nestlevel); > static Oid GetIndexOpClass(List *opclass, Oid attrType, > char *accessMethodName, Oid accessMethodId); > static char *ChooseIndexName(const char *tabname, Oid namespaceId, > @@ -176,8 +179,7 @@ > * Compute the operator classes, collations, and exclusion operators for > * the new index, so we can test whether it's compatible with the existing > * one. Note that ComputeIndexAttrs might fail here, but that's OK: > - * DefineIndex would have called this function with the same arguments > - * later on, and it would have failed then anyway. > + * DefineIndex would have failed later. > */ > indexInfo = makeNode(IndexInfo); > indexInfo->ii_Expressions = NIL; > @@ -195,7 +197,7 @@ > coloptions, attributeList, > exclusionOpNames, relationId, > accessMethodName, accessMethodId, > - amcanorder, isconstraint); > + amcanorder, isconstraint, InvalidOid, 0, NULL); > > > /* Get the soon-obsolete pg_index tuple. */ > @@ -288,6 +290,19 @@ > * DefineIndex > * Creates a new index. > * > + * This function manages the current userid according to the needs of pg_dump. > + * Recreating old-database catalog entries in new-database is fine, regardless > + * of which users would have permission to recreate those entries now. That's > + * just preservation of state. Running opaque expressions, like calling a > + * function named in a catalog entry or evaluating a pg_node_tree in a catalog > + * entry, as anyone other than the object owner, is not fine. To adhere to > + * those principles and to remain fail-safe, use the table owner userid for > + * most ACL checks. Use the original userid for ACL checks reached without > + * traversing opaque expressions. (pg_dump can predict such ACL checks from > + * catalogs.) Overall, this is a mess. Future DDL development should > + * consider offering one DDL command for catalog setup and a separate DDL > + * command for steps that run opaque expressions. > + * > * 'relationId': the OID of the heap relation on which the index is to be > * created > * 'stmt': IndexStmt describing the properties of the new index. > @@ -598,7 +613,8 @@ > coloptions, stmt->indexParams, > stmt->excludeOpNames, relationId, > accessMethodName, accessMethodId, > - amcanorder, stmt->isconstraint); > + amcanorder, stmt->isconstraint, root_save_userid, > + root_save_sec_context, &root_save_nestlevel); > > /* > * Extra checks when creating a PRIMARY KEY index. > @@ -706,9 +722,8 @@ > > /* > * Roll back any GUC changes executed by index functions, and keep > - * subsequent changes local to this command. It's barely possible that > - * some index function changed a behavior-affecting GUC, e.g. xmloption, > - * that affects subsequent steps. > + * subsequent changes local to this command. This is essential if some > + * index function changed a behavior-affecting GUC, e.g. search_path. > */ > AtEOXact_GUC(false, root_save_nestlevel); > root_save_nestlevel = NewGUCNestLevel(); > @@ -1063,6 +1078,10 @@ > /* > * Compute per-index-column information, including indexed column numbers > * or index expressions, opclasses, and indoptions. > + * > + * If the caller switched to the table owner, ddl_userid is the role for ACL > + * checks reached without traversing opaque expressions. Otherwise, it's > + * InvalidOid, and other ddl_* arguments are undefined. > */ > static void > ComputeIndexAttrs(IndexInfo *indexInfo, > @@ -1076,11 +1095,16 @@ > char *accessMethodName, > Oid accessMethodId, > bool amcanorder, > - bool isconstraint) > + bool isconstraint, > + Oid ddl_userid, > + int ddl_sec_context, > + int *ddl_save_nestlevel) > { > ListCell *nextExclOp; > ListCell *lc; > int attn; > + Oid save_userid; > + int save_sec_context; > > /* Allocate space for exclusion operator info, if needed */ > if (exclusionOpNames) > @@ -1096,6 +1120,9 @@ > else > nextExclOp = NULL; > > + if (OidIsValid(ddl_userid)) > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + > /* > * process attributeList > */ > @@ -1190,10 +1217,24 @@ > typeOidP[attn] = atttype; > > /* > - * Apply collation override if any > + * Apply collation override if any. Use of ddl_userid is necessary > + * due to ACL checks therein, and it's safe because collations don't > + * contain opaque expressions (or non-opaque expressions). > */ > if (attribute->collation) > + { > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > attcollation = get_collation_oid(attribute->collation, false); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > + } > > /* > * Check we have a collation iff it's a collatable type. The only > @@ -1221,12 +1262,25 @@ > collationOidP[attn] = attcollation; > > /* > - * Identify the opclass to use. > + * Identify the opclass to use. Use of ddl_userid is necessary due to > + * ACL checks therein. This is safe despite opclasses containing > + * opaque expressions (specifically, functions), because only > + * superusers can define opclasses. > */ > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > classOidP[attn] = GetIndexOpClass(attribute->opclass, > atttype, > accessMethodName, > accessMethodId); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > > /* > * Identify the exclusion operator, if any. > @@ -1240,9 +1294,23 @@ > > /* > * Find the operator --- it must accept the column datatype > - * without runtime coercion (but binary compatibility is OK) > + * without runtime coercion (but binary compatibility is OK). > + * Operators contain opaque expressions (specifically, functions). > + * compatible_oper_opid() boils down to oper() and > + * IsBinaryCoercible(). PostgreSQL would have security problems > + * elsewhere if oper() started calling opaque expressions. > */ > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > opid = compatible_oper_opid(opname, atttype, atttype, false); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > > /* > * Only allow commutative operators to be used in exclusion > From ef792f7856dea2576dcd9cab92b2b05fe955696b Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Mon, 9 May 2022 08:35:08 -0700 > Subject: [PATCH] Make relation-enumerating operations be security-restricted > operations. > > When a feature enumerates relations and runs functions associated with > all found relations, the feature's user shall not need to trust every > user having permission to create objects. BRIN-specific functionality > in autovacuum neglected to account for this, as did pg_amcheck and > CLUSTER. An attacker having permission to create non-temp objects in at > least one schema could execute arbitrary SQL functions under the > identity of the bootstrap superuser. CREATE INDEX (not a > relation-enumerating operation) and REINDEX protected themselves too > late. This change extends to the non-enumerating amcheck interface. > Back-patch to v10 (all supported versions). > > Sergey Shinderuk, reviewed (in earlier versions) by Alexander Lakhin. > Reported by Alexander Lakhin. > > Security: CVE-2022-1552 > --- > src/backend/catalog/index.c | 41 +++++++++++++++++++-------- > src/backend/commands/cluster.c | 35 ++++++++++++++++++----- > src/backend/commands/indexcmds.c | 47 +++++++++++++++++++++++++++++-- > src/backend/utils/init/miscinit.c | 24 +++++++++------ > src/test/regress/expected/privileges.out | 35 +++++++++++++++++++++++ > src/test/regress/sql/privileges.sql | 34 ++++++++++++++++++++++ > 6 files changed, 187 insertions(+), 29 deletions(-) > > --- a/src/backend/catalog/index.c > +++ b/src/backend/catalog/index.c > @@ -2743,7 +2743,17 @@ > > /* Open and lock the parent heap relation */ > heapRelation = heap_open(heapId, ShareUpdateExclusiveLock); > - /* And the target index relation */ > + > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > indexRelation = index_open(indexId, RowExclusiveLock); > > /* > @@ -2757,16 +2767,6 @@ > indexInfo->ii_Concurrent = true; > > /* > - * Switch to the table owner's userid, so that any index functions are run > - * as that user. Also lock down security-restricted operations and > - * arrange to make GUC variable changes local to this command. > - */ > - GetUserIdAndSecContext(&save_userid, &save_sec_context); > - SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > - save_sec_context | SECURITY_RESTRICTED_OPERATION); > - save_nestlevel = NewGUCNestLevel(); > - > - /* > * Scan the index and gather up all the TIDs into a tuplesort object. > */ > ivinfo.index = indexRelation; > @@ -3178,6 +3178,9 @@ > Relation iRel, > heapRelation; > Oid heapId; > + Oid save_userid; > + int save_sec_context; > + int save_nestlevel; > IndexInfo *indexInfo; > volatile bool skipped_constraint = false; > > @@ -3189,6 +3192,16 @@ > heapRelation = heap_open(heapId, ShareLock); > > /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(heapRelation->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > + /* > * Open the target index relation and get an exclusive lock on it, to > * ensure that no one else is touching this particular index. > */ > @@ -3324,6 +3337,12 @@ > heap_close(pg_index, RowExclusiveLock); > } > > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(save_userid, save_sec_context); > + > /* Close rels, but keep locks */ > index_close(iRel, NoLock); > heap_close(heapRelation, NoLock); > --- a/src/backend/commands/cluster.c > +++ b/src/backend/commands/cluster.c > @@ -41,6 +41,7 @@ > #include "storage/smgr.h" > #include "utils/acl.h" > #include "utils/fmgroids.h" > +#include "utils/guc.h" > #include "utils/inval.h" > #include "utils/lsyscache.h" > #include "utils/memutils.h" > @@ -259,6 +260,9 @@ > cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose) > { > Relation OldHeap; > + Oid save_userid; > + int save_sec_context; > + int save_nestlevel; > > /* Check for user-requested abort. */ > CHECK_FOR_INTERRUPTS(); > @@ -276,6 +280,16 @@ > return; > > /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(OldHeap->rd_rel->relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > + > + /* > * Since we may open a new transaction for each relation, we have to check > * that the relation still is what we think it is. > * > @@ -289,10 +303,10 @@ > Form_pg_index indexForm; > > /* Check that the user still owns the relation */ > - if (!pg_class_ownercheck(tableOid, GetUserId())) > + if (!pg_class_ownercheck(tableOid, save_userid)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -306,7 +320,7 @@ > if (RELATION_IS_OTHER_TEMP(OldHeap)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > if (OidIsValid(indexOid)) > @@ -317,7 +331,7 @@ > if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -327,14 +341,14 @@ > if (!HeapTupleIsValid(tuple)) /* probably can't happen */ > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > indexForm = (Form_pg_index) GETSTRUCT(tuple); > if (!indexForm->indisclustered) > { > ReleaseSysCache(tuple); > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > ReleaseSysCache(tuple); > } > @@ -388,7 +402,7 @@ > !RelationIsPopulated(OldHeap)) > { > relation_close(OldHeap, AccessExclusiveLock); > - return; > + goto out; > } > > /* > @@ -403,6 +417,13 @@ > rebuild_relation(OldHeap, indexOid, verbose); > > /* NB: rebuild_relation does heap_close() on OldHeap */ > + > +out: > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(save_userid, save_sec_context); > } > > /* > --- a/src/backend/commands/indexcmds.c > +++ b/src/backend/commands/indexcmds.c > @@ -44,6 +44,7 @@ > #include "utils/acl.h" > #include "utils/builtins.h" > #include "utils/fmgroids.h" > +#include "utils/guc.h" > #include "utils/inval.h" > #include "utils/lsyscache.h" > #include "utils/memutils.h" > @@ -329,8 +330,13 @@ > LOCKTAG heaplocktag; > LOCKMODE lockmode; > Snapshot snapshot; > + Oid root_save_userid; > + int root_save_sec_context; > + int root_save_nestlevel; > int i; > > + root_save_nestlevel = NewGUCNestLevel(); > + > /* > * Force non-concurrent build on temporary relations, even if CONCURRENTLY > * was requested. Other backends can't access a temporary relation, so > @@ -371,6 +377,15 @@ > lockmode = concurrent ? ShareUpdateExclusiveLock : ShareLock; > rel = heap_open(relationId, lockmode); > > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations. We > + * already arranged to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&root_save_userid, &root_save_sec_context); > + SetUserIdAndSecContext(rel->rd_rel->relowner, > + root_save_sec_context | SECURITY_RESTRICTED_OPERATION); > + > relationId = RelationGetRelid(rel); > namespaceId = RelationGetNamespace(rel); > > @@ -412,7 +427,7 @@ > { > AclResult aclresult; > > - aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), > + aclresult = pg_namespace_aclcheck(namespaceId, root_save_userid, > ACL_CREATE); > if (aclresult != ACLCHECK_OK) > aclcheck_error(aclresult, ACL_KIND_NAMESPACE, > @@ -439,7 +454,7 @@ > { > AclResult aclresult; > > - aclresult = pg_tablespace_aclcheck(tablespaceId, GetUserId(), > + aclresult = pg_tablespace_aclcheck(tablespaceId, root_save_userid, > ACL_CREATE); > if (aclresult != ACLCHECK_OK) > aclcheck_error(aclresult, ACL_KIND_TABLESPACE, > @@ -622,11 +637,23 @@ > skip_build || concurrent, > concurrent, !check_rights); > > + /* > + * Roll back any GUC changes executed by index functions, and keep > + * subsequent changes local to this command. It's barely possible that > + * some index function changed a behavior-affecting GUC, e.g. xmloption, > + * that affects subsequent steps. > + */ > + AtEOXact_GUC(false, root_save_nestlevel); > + root_save_nestlevel = NewGUCNestLevel(); > + > /* Add any requested comment */ > if (stmt->idxcomment != NULL) > CreateComments(indexRelationId, RelationRelationId, 0, > stmt->idxcomment); > > + AtEOXact_GUC(false, root_save_nestlevel); > + SetUserIdAndSecContext(root_save_userid, root_save_sec_context); > + > if (!concurrent) > { > /* Close the heap and we're done, in the non-concurrent case */ > @@ -705,6 +732,16 @@ > /* Open and lock the parent heap relation */ > rel = heap_openrv(stmt->relation, ShareUpdateExclusiveLock); > > + /* > + * Switch to the table owner's userid, so that any index functions are run > + * as that user. Also lock down security-restricted operations and > + * arrange to make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&root_save_userid, &root_save_sec_context); > + SetUserIdAndSecContext(rel->rd_rel->relowner, > + root_save_sec_context | SECURITY_RESTRICTED_OPERATION); > + root_save_nestlevel = NewGUCNestLevel(); > + > /* And the target index relation */ > indexRelation = index_open(indexRelationId, RowExclusiveLock); > > @@ -720,6 +757,12 @@ > /* Now build the index */ > index_build(rel, indexRelation, indexInfo, stmt->primary, false); > > + /* Roll back any GUC changes executed by index functions */ > + AtEOXact_GUC(false, root_save_nestlevel); > + > + /* Restore userid and security context */ > + SetUserIdAndSecContext(root_save_userid, root_save_sec_context); > + > /* Close both the relations, but keep the locks */ > heap_close(rel, NoLock); > index_close(indexRelation, NoLock); > --- a/src/backend/utils/init/miscinit.c > +++ b/src/backend/utils/init/miscinit.c > @@ -235,15 +235,21 @@ > * with guc.c's internal state, so SET ROLE has to be disallowed. > * > * SECURITY_RESTRICTED_OPERATION indicates that we are inside an operation > - * that does not wish to trust called user-defined functions at all. This > - * bit prevents not only SET ROLE, but various other changes of session state > - * that normally is unprotected but might possibly be used to subvert the > - * calling session later. An example is replacing an existing prepared > - * statement with new code, which will then be executed with the outer > - * session's permissions when the prepared statement is next used. Since > - * these restrictions are fairly draconian, we apply them only in contexts > - * where the called functions are really supposed to be side-effect-free > - * anyway, such as VACUUM/ANALYZE/REINDEX. > + * that does not wish to trust called user-defined functions at all. The > + * policy is to use this before operations, e.g. autovacuum and REINDEX, that > + * enumerate relations of a database or schema and run functions associated > + * with each found relation. The relation owner is the new user ID. Set this > + * as soon as possible after locking the relation. Restore the old user ID as > + * late as possible before closing the relation; restoring it shortly after > + * close is also tolerable. If a command has both relation-enumerating and > + * non-enumerating modes, e.g. ANALYZE, both modes set this bit. This bit > + * prevents not only SET ROLE, but various other changes of session state that > + * normally is unprotected but might possibly be used to subvert the calling > + * session later. An example is replacing an existing prepared statement with > + * new code, which will then be executed with the outer session's permissions > + * when the prepared statement is next used. These restrictions are fairly > + * draconian, but the functions called in relation-enumerating operations are > + * really supposed to be side-effect-free anyway. > * > * Unlike GetUserId, GetUserIdAndSecContext does *not* Assert that the current > * value of CurrentUserId is valid; nor does SetUserIdAndSecContext require > --- a/src/test/regress/expected/privileges.out > +++ b/src/test/regress/expected/privileges.out > @@ -1194,6 +1194,41 @@ > -- security-restricted operations > \c - > CREATE ROLE regress_sro_user; > +-- Check that index expressions and predicates are run as the table's owner > +-- A dummy index function checking current_user > +CREATE FUNCTION sro_ifun(int) RETURNS int AS $$ > +BEGIN > + -- Below we set the table's owner to regress_sro_user > + IF current_user <> 'regress_sro_user' THEN > + RAISE 'called by %', current_user; > + END IF; > + RETURN $1; > +END; > +$$ LANGUAGE plpgsql IMMUTABLE; > +-- Create a table owned by regress_sro_user > +CREATE TABLE sro_tab (a int); > +ALTER TABLE sro_tab OWNER TO regress_sro_user; > +INSERT INTO sro_tab VALUES (1), (2), (3); > +-- Create an expression index with a predicate > +CREATE INDEX sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +DROP INDEX sro_idx; > +-- Do the same concurrently > +CREATE INDEX CONCURRENTLY sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +-- REINDEX > +REINDEX TABLE sro_tab; > +REINDEX INDEX sro_idx; > +REINDEX TABLE CONCURRENTLY sro_tab; -- v12+ feature > +ERROR: syntax error at or near "CONCURRENTLY" > +LINE 1: REINDEX TABLE CONCURRENTLY sro_tab; > + ^ > +DROP INDEX sro_idx; > +-- CLUSTER > +CREATE INDEX sro_cluster_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))); > +CLUSTER sro_tab USING sro_cluster_idx; > +DROP INDEX sro_cluster_idx; > +DROP TABLE sro_tab; > SET SESSION AUTHORIZATION regress_sro_user; > CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS > 'GRANT regressgroup2 TO regress_sro_user'; > --- a/src/test/regress/sql/privileges.sql > +++ b/src/test/regress/sql/privileges.sql > @@ -724,6 +724,40 @@ > \c - > CREATE ROLE regress_sro_user; > > +-- Check that index expressions and predicates are run as the table's owner > + > +-- A dummy index function checking current_user > +CREATE FUNCTION sro_ifun(int) RETURNS int AS $$ > +BEGIN > + -- Below we set the table's owner to regress_sro_user > + IF current_user <> 'regress_sro_user' THEN > + RAISE 'called by %', current_user; > + END IF; > + RETURN $1; > +END; > +$$ LANGUAGE plpgsql IMMUTABLE; > +-- Create a table owned by regress_sro_user > +CREATE TABLE sro_tab (a int); > +ALTER TABLE sro_tab OWNER TO regress_sro_user; > +INSERT INTO sro_tab VALUES (1), (2), (3); > +-- Create an expression index with a predicate > +CREATE INDEX sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +DROP INDEX sro_idx; > +-- Do the same concurrently > +CREATE INDEX CONCURRENTLY sro_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))) > + WHERE sro_ifun(a + 10) > sro_ifun(10); > +-- REINDEX > +REINDEX TABLE sro_tab; > +REINDEX INDEX sro_idx; > +REINDEX TABLE CONCURRENTLY sro_tab; -- v12+ feature > +DROP INDEX sro_idx; > +-- CLUSTER > +CREATE INDEX sro_cluster_idx ON sro_tab ((sro_ifun(a) + sro_ifun(0))); > +CLUSTER sro_tab USING sro_cluster_idx; > +DROP INDEX sro_cluster_idx; > +DROP TABLE sro_tab; > + > SET SESSION AUTHORIZATION regress_sro_user; > CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS > 'GRANT regressgroup2 TO regress_sro_user'; > From f26d5702857a9c027f84850af48b0eea0f3aa15c Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Mon, 9 May 2022 08:35:08 -0700 > Subject: [PATCH] In REFRESH MATERIALIZED VIEW, set user ID before running user > code. > > It intended to, but did not, achieve this. Adopt the new standard of > setting user ID just after locking the relation. Back-patch to v10 (all > supported versions). > > Reviewed by Simon Riggs. Reported by Alvaro Herrera. > > Security: CVE-2022-1552 > --- > src/backend/commands/matview.c | 30 +++++++++++------------------- > src/test/regress/expected/privileges.out | 15 +++++++++++++++ > src/test/regress/sql/privileges.sql | 16 ++++++++++++++++ > 3 files changed, 42 insertions(+), 19 deletions(-) > > --- a/src/backend/commands/matview.c > +++ b/src/backend/commands/matview.c > @@ -161,6 +161,17 @@ > lockmode, false, false, > RangeVarCallbackOwnsTable, NULL); > matviewRel = heap_open(matviewOid, NoLock); > + relowner = matviewRel->rd_rel->relowner; > + > + /* > + * Switch to the owner's userid, so that any functions are run as that > + * user. Also lock down security-restricted operations and arrange to > + * make GUC variable changes local to this command. > + */ > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + SetUserIdAndSecContext(relowner, > + save_sec_context | SECURITY_RESTRICTED_OPERATION); > + save_nestlevel = NewGUCNestLevel(); > > /* Make sure it is a materialized view. */ > if (matviewRel->rd_rel->relkind != RELKIND_MATVIEW) > @@ -233,19 +244,6 @@ > */ > SetMatViewPopulatedState(matviewRel, !stmt->skipData); > > - relowner = matviewRel->rd_rel->relowner; > - > - /* > - * Switch to the owner's userid, so that any functions are run as that > - * user. Also arrange to make GUC variable changes local to this command. > - * Don't lock it down too tight to create a temporary table just yet. We > - * will switch modes when we are about to execute user code. > - */ > - GetUserIdAndSecContext(&save_userid, &save_sec_context); > - SetUserIdAndSecContext(relowner, > - save_sec_context | SECURITY_LOCAL_USERID_CHANGE); > - save_nestlevel = NewGUCNestLevel(); > - > /* Concurrent refresh builds new data in temp tablespace, and does diff. */ > if (concurrent) > tableSpace = GetDefaultTablespace(RELPERSISTENCE_TEMP); > @@ -262,12 +260,6 @@ > LockRelationOid(OIDNewHeap, AccessExclusiveLock); > dest = CreateTransientRelDestReceiver(OIDNewHeap); > > - /* > - * Now lock down security-restricted operations. > - */ > - SetUserIdAndSecContext(relowner, > - save_sec_context | SECURITY_RESTRICTED_OPERATION); > - > /* Generate the data, if wanted. */ > if (!stmt->skipData) > refresh_matview_datafill(dest, dataQuery, queryString); > --- a/src/test/regress/expected/privileges.out > +++ b/src/test/regress/expected/privileges.out > @@ -1266,6 +1266,21 @@ > SQL statement "SELECT unwanted_grant()" > PL/pgSQL function sro_trojan() line 1 at PERFORM > SQL function "mv_action" statement 1 > +-- REFRESH MATERIALIZED VIEW CONCURRENTLY use of eval_const_expressions() > +SET SESSION AUTHORIZATION regress_sro_user; > +CREATE FUNCTION unwanted_grant_nofail(int) RETURNS int > + IMMUTABLE LANGUAGE plpgsql AS $$ > +BEGIN > + PERFORM unwanted_grant(); > + RAISE WARNING 'owned'; > + RETURN 1; > +EXCEPTION WHEN OTHERS THEN > + RETURN 2; > +END$$; > +CREATE MATERIALIZED VIEW sro_index_mv AS SELECT 1 AS c; > +CREATE UNIQUE INDEX ON sro_index_mv (c) WHERE unwanted_grant_nofail(1) > 0; > +\c - > +REFRESH MATERIALIZED VIEW sro_index_mv; > DROP OWNED BY regress_sro_user; > DROP ROLE regress_sro_user; > -- Admin options > --- a/src/test/regress/sql/privileges.sql > +++ b/src/test/regress/sql/privileges.sql > @@ -784,6 +784,22 @@ > REFRESH MATERIALIZED VIEW sro_mv; > BEGIN; SET CONSTRAINTS ALL IMMEDIATE; REFRESH MATERIALIZED VIEW sro_mv; COMMIT; > > +-- REFRESH MATERIALIZED VIEW CONCURRENTLY use of eval_const_expressions() > +SET SESSION AUTHORIZATION regress_sro_user; > +CREATE FUNCTION unwanted_grant_nofail(int) RETURNS int > + IMMUTABLE LANGUAGE plpgsql AS $$ > +BEGIN > + PERFORM unwanted_grant(); > + RAISE WARNING 'owned'; > + RETURN 1; > +EXCEPTION WHEN OTHERS THEN > + RETURN 2; > +END$$; > +CREATE MATERIALIZED VIEW sro_index_mv AS SELECT 1 AS c; > +CREATE UNIQUE INDEX ON sro_index_mv (c) WHERE unwanted_grant_nofail(1) > 0; > +\c - > +REFRESH MATERIALIZED VIEW sro_index_mv; > + > DROP OWNED BY regress_sro_user; > DROP ROLE regress_sro_user; > > From 88b39e61486a8925a3861d50c306a51eaa1af8d6 Mon Sep 17 00:00:00 2001 > From: Noah Misch <noah@leadboat.com> > Date: Sat, 25 Jun 2022 09:07:41 -0700 > Subject: [PATCH] CREATE INDEX: use the original userid for more ACL checks. > > Commit a117cebd638dd02e5c2e791c25e43745f233111b used the original userid > for ACL checks located directly in DefineIndex(), but it still adopted > the table owner userid for more ACL checks than intended. That broke > dump/reload of indexes that refer to an operator class, collation, or > exclusion operator in a schema other than "public" or "pg_catalog". > Back-patch to v10 (all supported versions), like the earlier commit. > > Nathan Bossart and Noah Misch > > Discussion: https://postgr.es/m/f8a4105f076544c180a87ef0c4822352@stmuk.bayern.de > --- > contrib/citext/Makefile | 2 > contrib/citext/expected/create_index_acl.out | 81 +++++++++++++++++++++++ > contrib/citext/sql/create_index_acl.sql | 82 ++++++++++++++++++++++++ > src/backend/commands/indexcmds.c | 92 +++++++++++++++++++++++---- > 4 files changed, 244 insertions(+), 13 deletions(-) > create mode 100644 contrib/citext/expected/create_index_acl.out > create mode 100644 contrib/citext/sql/create_index_acl.sql > > --- a/contrib/citext/Makefile > +++ b/contrib/citext/Makefile > @@ -7,7 +7,7 @@ > citext--1.1--1.0.sql citext--unpackaged--1.0.sql > PGFILEDESC = "citext - case-insensitive character string data type" > > -REGRESS = citext > +REGRESS = create_index_acl citext > > ifdef USE_PGXS > PG_CONFIG = pg_config > --- /dev/null > +++ b/contrib/citext/expected/create_index_acl.out > @@ -0,0 +1,81 @@ > +-- Each DefineIndex() ACL check uses either the original userid or the table > +-- owner userid; see its header comment. Here, confirm that DefineIndex() > +-- uses its original userid where necessary. The test works by creating > +-- indexes that refer to as many sorts of objects as possible, with the table > +-- owner having as few applicable privileges as possible. (The privileges.sql > +-- regress_sro_user tests look for the opposite defect; they confirm that > +-- DefineIndex() uses the table owner userid where necessary.) > +-- Don't override tablespaces; this version lacks allow_in_place_tablespaces. > +BEGIN; > +CREATE ROLE regress_minimal; > +CREATE SCHEMA s; > +CREATE EXTENSION citext SCHEMA s; > +-- Revoke all conceivably-relevant ACLs within the extension. The system > +-- doesn't check all these ACLs, but this will provide some coverage if that > +-- ever changes. > +REVOKE ALL ON TYPE s.citext FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_lt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_le(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_eq(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_ge(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_gt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_cmp(s.citext, s.citext) FROM PUBLIC; > +-- Functions sufficient for making an index column that has the side effect of > +-- changing search_path at expression planning time. > +CREATE FUNCTION public.setter() RETURNS bool VOLATILE > + LANGUAGE SQL AS $$SET search_path = s; SELECT true$$; > +CREATE FUNCTION s.const() RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT public.setter()$$; > +CREATE FUNCTION s.index_this_expr(s.citext, bool) RETURNS s.citext IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1$$; > +REVOKE ALL ON FUNCTION public.setter() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.const() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.index_this_expr(s.citext, bool) FROM PUBLIC; > +-- Even for an empty table, expression planning calls s.const & public.setter. > +GRANT EXECUTE ON FUNCTION public.setter() TO regress_minimal; > +GRANT EXECUTE ON FUNCTION s.const() TO regress_minimal; > +-- Function for index predicate. > +CREATE FUNCTION s.index_row_if(s.citext) RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1 IS NOT NULL$$; > +REVOKE ALL ON FUNCTION s.index_row_if(s.citext) FROM PUBLIC; > +-- Even for an empty table, CREATE INDEX checks ii_Predicate permissions. > +GRANT EXECUTE ON FUNCTION s.index_row_if(s.citext) TO regress_minimal; > +-- Non-extension, non-function objects. > +CREATE COLLATION s.coll (LOCALE="C"); > +CREATE TABLE s.x (y s.citext); > +ALTER TABLE s.x OWNER TO regress_minimal; > +-- Empty-table DefineIndex() > +CREATE UNIQUE INDEX u0rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e0rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Make the table nonempty. > +INSERT INTO s.x VALUES ('foo'), ('bar'); > +-- If the INSERT runs the planner on index expressions, a search_path change > +-- survives. As of 2022-06, the INSERT reuses a cached plan. It does so even > +-- under debug_discard_caches, since each index is new-in-transaction. If > +-- future work changes a cache lifecycle, this RESET may become necessary. > +RESET search_path; > +-- For a nonempty table, owner needs permissions throughout ii_Expressions. > +GRANT EXECUTE ON FUNCTION s.index_this_expr(s.citext, bool) TO regress_minimal; > +CREATE UNIQUE INDEX u2rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e2rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Shall not find s.coll via search_path, despite the s.const->public.setter > +-- call having set search_path=s during expression planning. Suppress the > +-- message itself, which depends on the database encoding. > +\set VERBOSITY terse > +DO $$ > +BEGIN > +ALTER TABLE s.x ADD CONSTRAINT underqualified EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE coll WITH s.=) > + WHERE (s.index_row_if(y)); > +EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION '%', sqlstate; END$$; > +ERROR: 42704 > +\set VERBOSITY default > +ROLLBACK; > --- /dev/null > +++ b/contrib/citext/sql/create_index_acl.sql > @@ -0,0 +1,82 @@ > +-- Each DefineIndex() ACL check uses either the original userid or the table > +-- owner userid; see its header comment. Here, confirm that DefineIndex() > +-- uses its original userid where necessary. The test works by creating > +-- indexes that refer to as many sorts of objects as possible, with the table > +-- owner having as few applicable privileges as possible. (The privileges.sql > +-- regress_sro_user tests look for the opposite defect; they confirm that > +-- DefineIndex() uses the table owner userid where necessary.) > + > +-- Don't override tablespaces; this version lacks allow_in_place_tablespaces. > + > +BEGIN; > +CREATE ROLE regress_minimal; > +CREATE SCHEMA s; > +CREATE EXTENSION citext SCHEMA s; > +-- Revoke all conceivably-relevant ACLs within the extension. The system > +-- doesn't check all these ACLs, but this will provide some coverage if that > +-- ever changes. > +REVOKE ALL ON TYPE s.citext FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_lt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_le(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_eq(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_ge(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_gt(s.citext, s.citext) FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.citext_cmp(s.citext, s.citext) FROM PUBLIC; > +-- Functions sufficient for making an index column that has the side effect of > +-- changing search_path at expression planning time. > +CREATE FUNCTION public.setter() RETURNS bool VOLATILE > + LANGUAGE SQL AS $$SET search_path = s; SELECT true$$; > +CREATE FUNCTION s.const() RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT public.setter()$$; > +CREATE FUNCTION s.index_this_expr(s.citext, bool) RETURNS s.citext IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1$$; > +REVOKE ALL ON FUNCTION public.setter() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.const() FROM PUBLIC; > +REVOKE ALL ON FUNCTION s.index_this_expr(s.citext, bool) FROM PUBLIC; > +-- Even for an empty table, expression planning calls s.const & public.setter. > +GRANT EXECUTE ON FUNCTION public.setter() TO regress_minimal; > +GRANT EXECUTE ON FUNCTION s.const() TO regress_minimal; > +-- Function for index predicate. > +CREATE FUNCTION s.index_row_if(s.citext) RETURNS bool IMMUTABLE > + LANGUAGE SQL AS $$SELECT $1 IS NOT NULL$$; > +REVOKE ALL ON FUNCTION s.index_row_if(s.citext) FROM PUBLIC; > +-- Even for an empty table, CREATE INDEX checks ii_Predicate permissions. > +GRANT EXECUTE ON FUNCTION s.index_row_if(s.citext) TO regress_minimal; > +-- Non-extension, non-function objects. > +CREATE COLLATION s.coll (LOCALE="C"); > +CREATE TABLE s.x (y s.citext); > +ALTER TABLE s.x OWNER TO regress_minimal; > +-- Empty-table DefineIndex() > +CREATE UNIQUE INDEX u0rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e0rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Make the table nonempty. > +INSERT INTO s.x VALUES ('foo'), ('bar'); > +-- If the INSERT runs the planner on index expressions, a search_path change > +-- survives. As of 2022-06, the INSERT reuses a cached plan. It does so even > +-- under debug_discard_caches, since each index is new-in-transaction. If > +-- future work changes a cache lifecycle, this RESET may become necessary. > +RESET search_path; > +-- For a nonempty table, owner needs permissions throughout ii_Expressions. > +GRANT EXECUTE ON FUNCTION s.index_this_expr(s.citext, bool) TO regress_minimal; > +CREATE UNIQUE INDEX u2rows ON s.x USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll s.citext_ops) > + WHERE s.index_row_if(y); > +ALTER TABLE s.x ADD CONSTRAINT e2rows EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE s.coll WITH s.=) > + WHERE (s.index_row_if(y)); > +-- Shall not find s.coll via search_path, despite the s.const->public.setter > +-- call having set search_path=s during expression planning. Suppress the > +-- message itself, which depends on the database encoding. > +\set VERBOSITY terse > +DO $$ > +BEGIN > +ALTER TABLE s.x ADD CONSTRAINT underqualified EXCLUDE USING btree > + ((s.index_this_expr(y, s.const())) COLLATE coll WITH s.=) > + WHERE (s.index_row_if(y)); > +EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION '%', sqlstate; END$$; > +\set VERBOSITY default > +ROLLBACK; > --- a/src/backend/commands/indexcmds.c > +++ b/src/backend/commands/indexcmds.c > @@ -65,7 +65,10 @@ > Oid relId, > char *accessMethodName, Oid accessMethodId, > bool amcanorder, > - bool isconstraint); > + bool isconstraint, > + Oid ddl_userid, > + int ddl_sec_context, > + int *ddl_save_nestlevel); > static Oid GetIndexOpClass(List *opclass, Oid attrType, > char *accessMethodName, Oid accessMethodId); > static char *ChooseIndexName(const char *tabname, Oid namespaceId, > @@ -168,8 +171,7 @@ > * Compute the operator classes, collations, and exclusion operators for > * the new index, so we can test whether it's compatible with the existing > * one. Note that ComputeIndexAttrs might fail here, but that's OK: > - * DefineIndex would have called this function with the same arguments > - * later on, and it would have failed then anyway. > + * DefineIndex would have failed later. > */ > indexInfo = makeNode(IndexInfo); > indexInfo->ii_Expressions = NIL; > @@ -187,7 +189,7 @@ > coloptions, attributeList, > exclusionOpNames, relationId, > accessMethodName, accessMethodId, > - amcanorder, isconstraint); > + amcanorder, isconstraint, InvalidOid, 0, NULL); > > > /* Get the soon-obsolete pg_index tuple. */ > @@ -280,6 +282,19 @@ > * DefineIndex > * Creates a new index. > * > + * This function manages the current userid according to the needs of pg_dump. > + * Recreating old-database catalog entries in new-database is fine, regardless > + * of which users would have permission to recreate those entries now. That's > + * just preservation of state. Running opaque expressions, like calling a > + * function named in a catalog entry or evaluating a pg_node_tree in a catalog > + * entry, as anyone other than the object owner, is not fine. To adhere to > + * those principles and to remain fail-safe, use the table owner userid for > + * most ACL checks. Use the original userid for ACL checks reached without > + * traversing opaque expressions. (pg_dump can predict such ACL checks from > + * catalogs.) Overall, this is a mess. Future DDL development should > + * consider offering one DDL command for catalog setup and a separate DDL > + * command for steps that run opaque expressions. > + * > * 'relationId': the OID of the heap relation on which the index is to be > * created > * 'stmt': IndexStmt describing the properties of the new index. > @@ -581,7 +596,8 @@ > coloptions, stmt->indexParams, > stmt->excludeOpNames, relationId, > accessMethodName, accessMethodId, > - amcanorder, stmt->isconstraint); > + amcanorder, stmt->isconstraint, root_save_userid, > + root_save_sec_context, &root_save_nestlevel); > > /* > * Extra checks when creating a PRIMARY KEY index. > @@ -639,9 +655,8 @@ > > /* > * Roll back any GUC changes executed by index functions, and keep > - * subsequent changes local to this command. It's barely possible that > - * some index function changed a behavior-affecting GUC, e.g. xmloption, > - * that affects subsequent steps. > + * subsequent changes local to this command. This is essential if some > + * index function changed a behavior-affecting GUC, e.g. search_path. > */ > AtEOXact_GUC(false, root_save_nestlevel); > root_save_nestlevel = NewGUCNestLevel(); > @@ -996,6 +1011,10 @@ > /* > * Compute per-index-column information, including indexed column numbers > * or index expressions, opclasses, and indoptions. > + * > + * If the caller switched to the table owner, ddl_userid is the role for ACL > + * checks reached without traversing opaque expressions. Otherwise, it's > + * InvalidOid, and other ddl_* arguments are undefined. > */ > static void > ComputeIndexAttrs(IndexInfo *indexInfo, > @@ -1009,11 +1028,16 @@ > char *accessMethodName, > Oid accessMethodId, > bool amcanorder, > - bool isconstraint) > + bool isconstraint, > + Oid ddl_userid, > + int ddl_sec_context, > + int *ddl_save_nestlevel) > { > ListCell *nextExclOp; > ListCell *lc; > int attn; > + Oid save_userid; > + int save_sec_context; > > /* Allocate space for exclusion operator info, if needed */ > if (exclusionOpNames) > @@ -1029,6 +1053,9 @@ > else > nextExclOp = NULL; > > + if (OidIsValid(ddl_userid)) > + GetUserIdAndSecContext(&save_userid, &save_sec_context); > + > /* > * process attributeList > */ > @@ -1123,10 +1150,24 @@ > typeOidP[attn] = atttype; > > /* > - * Apply collation override if any > + * Apply collation override if any. Use of ddl_userid is necessary > + * due to ACL checks therein, and it's safe because collations don't > + * contain opaque expressions (or non-opaque expressions). > */ > if (attribute->collation) > + { > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > attcollation = get_collation_oid(attribute->collation, false); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > + } > > /* > * Check we have a collation iff it's a collatable type. The only > @@ -1154,12 +1195,25 @@ > collationOidP[attn] = attcollation; > > /* > - * Identify the opclass to use. > + * Identify the opclass to use. Use of ddl_userid is necessary due to > + * ACL checks therein. This is safe despite opclasses containing > + * opaque expressions (specifically, functions), because only > + * superusers can define opclasses. > */ > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > classOidP[attn] = GetIndexOpClass(attribute->opclass, > atttype, > accessMethodName, > accessMethodId); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > > /* > * Identify the exclusion operator, if any. > @@ -1173,9 +1227,23 @@ > > /* > * Find the operator --- it must accept the column datatype > - * without runtime coercion (but binary compatibility is OK) > + * without runtime coercion (but binary compatibility is OK). > + * Operators contain opaque expressions (specifically, functions). > + * compatible_oper_opid() boils down to oper() and > + * IsBinaryCoercible(). PostgreSQL would have security problems > + * elsewhere if oper() started calling opaque expressions. > */ > + if (OidIsValid(ddl_userid)) > + { > + AtEOXact_GUC(false, *ddl_save_nestlevel); > + SetUserIdAndSecContext(ddl_userid, ddl_sec_context); > + } > opid = compatible_oper_opid(opname, atttype, atttype, false); > + if (OidIsValid(ddl_userid)) > + { > + SetUserIdAndSecContext(save_userid, save_sec_context); > + *ddl_save_nestlevel = NewGUCNestLevel(); > + } > > /* > * Only allow commutative operators to be used in exclusion -- Roberto C. Sánchez http://people.connexer.com/~roberto http://www.connexer.com
В списке pgsql-hackers по дате отправления:
Предыдущее
От: Robert HaasДата:
Сообщение: Re: [Commitfest 2022-07] Patch Triage: Waiting on Author