This is an automated email from the ASF dual-hosted git repository.

reshke pushed a commit to branch backport_cve
in repository https://gitbox.apache.org/repos/asf/cloudberry.git

commit 40f76ca3e9d2833aab192d833928d74d267eb300
Author: Noah Misch <[email protected]>
AuthorDate: Mon Aug 10 06:38:24 2026 -0700

    Empty search_path in amcheck.
    
    A grantee of amcheck function EXECUTE privilege could execute arbitrary
    functions as the owners of expression indexes that depend on the search
    path.  An expression like (lower(col)) was not vulnerable, because
    lower() is resolved at CREATE INDEX time.  However, an expression
    calling an sql-language or plpgsql-language function often was
    vulnerable, even if it used search_path only to find objects in
    pg_catalog.  The amcheck documentation has been warning about data
    disclosure after such a GRANT, not about function execution.
    
    This might cause new amcheck errors when index expressions rely on a
    broader search_path.  Such indexes have seen errors during auto-analyze
    since CVE-2018-1058 commit 582edc369cdbd348d68441fc50fa26a84afd0c1a, and
    v17 amcheck always worked this way.  Hence, the risk is low.
    
    Leave a comment on the one other sandbox entrance that doesn't empty
    search_path.  In its case, the choice was valid.
    
    Back-patch to v14 (all supported versions), but v17 was safe already.
    Commit 2af07e2f749a9208ca1ed84fa1d8fe0e75833288 (v17) unintentionally
    blocked the attack, and commit d70b17636ddf1ea2c71d1c7bc477372b36ccb66b
    (v18) unintentionally removed that protection.  Hence, this adds to v17
    just a test and a comment.  While emptying search_path became more
    widespread in commit 2af07e2f749a9208ca1ed84fa1d8fe0e75833288 (v17),
    none of its other changes blocked an attack available in v16, even when
    considering GRANT.  For example, brin_summarize_range() has had an owner
    check that GRANT does not override.
    
    Reported-by: 王跃林 <[email protected]>
    Reported-by: Jacob Brazeal <[email protected]>
    Backpatch-through: 14
    Security: CVE-2026-14673
---
 contrib/amcheck/expected/check_btree.out | 17 +++++++++++++++--
 contrib/amcheck/sql/check_btree.sql      | 18 +++++++++++++++---
 contrib/amcheck/verify_nbtree.c          |  2 ++
 src/backend/utils/init/usercontext.c     |  6 ++++++
 4 files changed, 38 insertions(+), 5 deletions(-)

diff --git a/contrib/amcheck/expected/check_btree.out 
b/contrib/amcheck/expected/check_btree.out
index 7c7fbcdfc7d..76b570db96b 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -186,7 +186,8 @@ SELECT bt_index_check('toasty', true);
 (1 row)
 
 --
--- Check that index expressions and predicates are run as the table's owner
+-- Check that index expressions and predicates are run as the table's owner,
+-- with empty search_path
 --
 TRUNCATE bttest_a;
 INSERT INTO bttest_a SELECT * FROM generate_series(1, 1000);
@@ -194,19 +195,31 @@ ALTER TABLE bttest_a OWNER TO regress_bttest_role;
 -- A dummy index function checking current_user
 CREATE FUNCTION ifun(int8) RETURNS int8 AS $$
 BEGIN
-       ASSERT current_user = 'regress_bttest_role',
+       ASSERT current_setting('search_path') NOT LIKE '%preempt%',
+               format('ifun(%s) called with current_schemas %s, search_path 
%s',
+                       $1, current_schemas(true), 
current_setting('search_path'));
+       ASSERT "current_user"() = 'regress_bttest_role',
                format('ifun(%s) called by %s', $1, current_user);
        RETURN $1;
 END;
 $$ LANGUAGE plpgsql IMMUTABLE;
 CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
        WHERE ifun(id + 10) > ifun(10);
+BEGIN;
+SET LOCAL check_function_bodies = off;
+CREATE SCHEMA preempt;
+GRANT USAGE ON SCHEMA preempt TO regress_bttest_role;
+SET LOCAL search_path = preempt, pg_catalog, public;
+CREATE FUNCTION "current_user"() RETURNS name AS $$
+       broken
+$$ LANGUAGE sql STABLE PARALLEL SAFE STRICT;
 SELECT bt_index_check('bttest_a_expr_idx', true);
  bt_index_check 
 ----------------
  
 (1 row)
 
+ROLLBACK;
 -- Check support of both 1B and 4B header sizes of short varlena datum
 CREATE TABLE varlena_bug (v text);
 ALTER TABLE varlena_bug ALTER column v SET storage plain;
diff --git a/contrib/amcheck/sql/check_btree.sql 
b/contrib/amcheck/sql/check_btree.sql
index bc2224ea49d..04796573c9a 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -117,7 +117,8 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 SELECT bt_index_check('toasty', true);
 
 --
--- Check that index expressions and predicates are run as the table's owner
+-- Check that index expressions and predicates are run as the table's owner,
+-- with empty search_path
 --
 TRUNCATE bttest_a;
 INSERT INTO bttest_a SELECT * FROM generate_series(1, 1000);
@@ -125,7 +126,10 @@ ALTER TABLE bttest_a OWNER TO regress_bttest_role;
 -- A dummy index function checking current_user
 CREATE FUNCTION ifun(int8) RETURNS int8 AS $$
 BEGIN
-       ASSERT current_user = 'regress_bttest_role',
+       ASSERT current_setting('search_path') NOT LIKE '%preempt%',
+               format('ifun(%s) called with current_schemas %s, search_path 
%s',
+                       $1, current_schemas(true), 
current_setting('search_path'));
+       ASSERT "current_user"() = 'regress_bttest_role',
                format('ifun(%s) called by %s', $1, current_user);
        RETURN $1;
 END;
@@ -133,8 +137,16 @@ $$ LANGUAGE plpgsql IMMUTABLE;
 
 CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
        WHERE ifun(id + 10) > ifun(10);
-
+BEGIN;
+SET LOCAL check_function_bodies = off;
+CREATE SCHEMA preempt;
+GRANT USAGE ON SCHEMA preempt TO regress_bttest_role;
+SET LOCAL search_path = preempt, pg_catalog, public;
+CREATE FUNCTION "current_user"() RETURNS name AS $$
+       broken
+$$ LANGUAGE sql STABLE PARALLEL SAFE STRICT;
 SELECT bt_index_check('bttest_a_expr_idx', true);
+ROLLBACK;
 
 -- Check support of both 1B and 4B header sizes of short varlena datum
 CREATE TABLE varlena_bug (v text);
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d92a9466224..fbd25760bc1 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -286,6 +286,8 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, 
bool heapallindexed,
                SetUserIdAndSecContext(heaprel->rd_rel->relowner,
                                                           save_sec_context | 
SECURITY_RESTRICTED_OPERATION);
                save_nestlevel = NewGUCNestLevel();
+               set_config_option("search_path", "pg_catalog, pg_temp", 
PGC_USERSET,
+                                                 PGC_S_SESSION, 
GUC_ACTION_SAVE, true, 0, false);
        }
        else
        {
diff --git a/src/backend/utils/init/usercontext.c 
b/src/backend/utils/init/usercontext.c
index dd9a0dd6a83..1a3abc62ae8 100644
--- a/src/backend/utils/init/usercontext.c
+++ b/src/backend/utils/init/usercontext.c
@@ -70,6 +70,12 @@ SwitchToUntrustedUser(Oid userid, UserContext *context)
                 * session state. Also set up a new GUC nest level, so that we 
can
                 * roll back any GUC changes that may be made by code running 
as the
                 * target user, inasmuch as they could be malicious.
+                *
+                * Unlike most use of SECURITY_RESTRICTED_OPERATION, this opts 
not to
+                * use RestrictSearchPath().  Calling it would just stop the 
current
+                * user from attacking "userid", but we've already established 
that
+                * the current user could just SET ROLE to "userid".  Calling 
it would
+                * be a compatibility break.
                 */
                sec_context |= SECURITY_RESTRICTED_OPERATION;
                SetUserIdAndSecContext(userid, sec_context);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to