Hi Hackers,

We would like to propose hardening name resolution during extension
installation and upgrade scripts, so that a script cannot be made to
reach an object that another role planted in the extension's
installation schema.


Problem
=======

A CREATE EXTENSION or ALTER EXTENSION ... UPDATE script runs as the
invoking role, or as the bootstrap superuser for a trusted extension
installed by a non-superuser. Either way a captured reference runs
another role's code with elevated rights.

Say the script calls f('abc'). The literal has type "unknown", so if
the extension defines f(varchar) but another role has already created
f(text) in the same schema, both are candidates. They sit in the same
schema, so search-path position cannot break the tie, and
func_select_candidate() falls back to type preference: text is the
preferred type of the string category, so f(text) wins, and the script
runs that role's function with its own privileges. Writing
@[email protected]('abc') changes nothing: qualification pins the schema the
plant sits in, not the signature.

Neither existing defense covers this. The search_path pinning from
7eeb1d9861b ("Make contrib modules' installation scripts more
secure.") only keeps the plant out of the other schemas on the path,
not out of the extension's own target schema, which is necessarily
first. And the ownership checks from b9b21acc766 ("In extensions,
don't replace objects not belonging to the extension.") guard what a
script creates, not what it references.

The same applies to operators: CREATE OPERATOR only requires a function
the creating role can execute. Nor is it limited to overloads: a domain
or table planted under the name of a required extension's type or config
table captures the reference outright, and its CHECK constraint or
triggers then run with the script's privileges.

The only precondition is that some other role can create objects in
the installation schema. Hosted platforms that run whitelisted
extension scripts as superuser on a user's behalf make that the normal
case, not an odd configuration.

This hazard is documented: "Security Considerations for Extension
Scripts" describes these trojan objects and tells authors to
schema-qualify every name and add explicit casts [1]. That advice is
correct, but it puts the whole burden on the author getting every call
site right, and one uncast call is enough, so a resolution-time
backstop seems worthwhile.


Proposed change
===============

Two patches attached:

0001 - propagates the extension-script state to parallel workers.
creating_extension and CurrentExtensionObject are backend-local
globals, so a worker never saw them. Work that a worker does on behalf
of a script, such as parse-analyzing a parallel-safe function's body at
run time, therefore ran as though no script were in progress and would
skip the check that 0002 adds. Both values now ride through
FixedParallelState the same way the current user id and the
temp-namespace state already do. No visible effect on its own.


0002 - makes name resolution ignore untrusted objects while
creating_extension is set. An object is trusted if it is in pg_catalog,
owned by a superuser, owned by the role the script is running as, or a
member of the extension being installed or of one it requires; the last
rule reads pg_depend, and only where the cheaper tests fail.

The search-path lookups for relations, types, functions and operators
all apply the test. For functions and operators it happens as
candidates are gathered, not in the callers that resolve the
ambiguity: the gather loop collapses duplicate signatures to the one
earliest on the path, so a plant in the extension's own schema would
otherwise displace a required extension's identical function. It is
also the single place every caller passes through, including
LookupFuncNameInternal() and regprocedure input; the operator test in
OpernameGetOprid() likewise covers binary_oper_exact(),
LookupOperName(), and so CREATE OPERATOR CLASS, and regoperator.

Caches need the same treatment. oper() and left_oper() skip the
operator lookaside cache while creating_extension, and CachedPlanSource
records whether it was analyzed inside a script, including on the
plpgsql simple-expression fast path, since a plan analyzed beforehand
in a session whose search_path matches the one the script pins would
otherwise be reused inside it.

If nothing trusted remains, resolution fails as though the object did
not exist, with a detail saying a candidate was ignored. If a trusted
candidate exists but does not match, the usual argument-type error is
raised and the ignored candidate is mentioned in a hint. Outside
extension scripts every object is trusted, so ordinary parsing is
unchanged.

0002 adds regression tests for planted overloads, references with no
trusted candidate, "superuser = false" extensions, updates run by
another role, required extensions, cached plans and parallel workers.


What this does not cover
========================

The filtering only applies while the script runs. An extension's own
function bodies resolve names when they execute, with
creating_extension false again, so a planted overload can still
capture those calls after installation, usually with the caller's
privileges, though SECURITY DEFINER puts the elevated case back on the
table. Covering that wants a different mechanism; script time seems
worth doing on its own, being the window where a captured reference is
most likely to run as a superuser.

Trust is by ownership, not by name. A script that references an object
owned by another ordinary role, including one that belongs to an
extension outside its direct requires list, now fails even with a
schema-qualified name. Such references should be rare, but this is a
behavior change for existing scripts.

Operator classes and families, collations, text search objects,
conversions, statistics objects and casts still take unfiltered lookup
paths; none looked like a route to running an unprivileged role's code,
but we may have missed one. The lookups that resolve by exact name also
have no flags word to carry the "candidate ignored" detail.


Related work
============

Jelte Fennema-Nio's "extensions with an owned schema" [2] starts from
the same observation and gives the extension a fresh schema, removing
the precondition. It is opt-in and only for new extensions, so the two
look complementary.

The "sandboxing untrusted code" thread [3] makes a point we tried to
honor: a check that rejects what a human reads as harmless gets
switched off. A script references built-ins, its own objects and its
required extensions', and the trust rule accepts all three regardless
of who owns them.


Does this approach seem reasonable?


[1]
https://www.postgresql.org/docs/current/extend-extensions.html#EXTEND-EXTENSIONS-SECURITY-SCRIPTS
[2]
https://www.postgresql.org/message-id/flat/CAGECzQQzDqDzakBkR71ZkQ1N1ffTjAaruRSqppQAKu3WF%2B6rNQ%40mail.gmail.com
[3]
https://www.postgresql.org/message-id/flat/CA%2BTgmoYiumw-yR8nUUX_8qdihPd0ZmT29ch0VR_r%2Bkw%2Bo7QJvQ%40mail.gmail.com


Best regards
Jan Nidzwetzki
Fabrízio de Royes Mello

-- 
Jan Nidzwetzki
PlanetScale Postgres Core Team
From 4658f72d6a1e4a9e0e91fa45e787a3ec186d3176 Mon Sep 17 00:00:00 2001
From: Jan Nidzwetzki <[email protected]>
Date: Wed, 5 Aug 2026 15:21:09 +0200
Subject: [PATCH 1/2] Propagate extension-script state to parallel workers

creating_extension and CurrentExtensionObject are backend-local, so a
parallel worker doing work for a CREATE/ALTER EXTENSION script (for
example, executing a parallel-safe function's body) behaved as though no
script were in progress.

No visible effect on its own; preparation for the following commit.
---
 src/backend/access/transam/parallel.c |  9 +++++++++
 src/backend/commands/extension.c      | 24 ++++++++++++++++++++++++
 src/include/commands/extension.h      |  3 +++
 3 files changed, 36 insertions(+)

diff --git a/src/backend/access/transam/parallel.c 
b/src/backend/access/transam/parallel.c
index 17fcd246b0c..c5f67b2f992 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/extension.h"
 #include "commands/vacuum.h"
 #include "executor/execParallel.h"
 #include "libpq/libpq.h"
@@ -90,9 +91,11 @@ typedef struct FixedParallelState
        Oid                     current_user_id;
        Oid                     temp_namespace_id;
        Oid                     temp_toast_namespace_id;
+       Oid                     current_extension_object;
        int                     sec_context;
        bool            session_user_is_superuser;
        bool            role_is_superuser;
+       bool            creating_extension;
        PGPROC     *parallel_leader_pgproc;
        pid_t           parallel_leader_pid;
        ProcNumber      parallel_leader_proc_number;
@@ -348,6 +351,8 @@ InitializeParallelDSM(ParallelContext *pcxt)
        fps->role_is_superuser = current_role_is_superuser;
        GetTempNamespaceState(&fps->temp_namespace_id,
                                                  
&fps->temp_toast_namespace_id);
+       GetExtensionCreationState(&fps->creating_extension,
+                                                         
&fps->current_extension_object);
        fps->parallel_leader_pgproc = MyProc;
        fps->parallel_leader_pid = MyProcPid;
        fps->parallel_leader_proc_number = MyProcNumber;
@@ -1534,6 +1539,10 @@ ParallelWorkerMain(Datum main_arg)
        SetTempNamespaceState(fps->temp_namespace_id,
                                                  fps->temp_toast_namespace_id);
 
+       /* Restore extension-script state, so the worker matches the leader. */
+       SetExtensionCreationState(fps->creating_extension,
+                                                         
fps->current_extension_object);
+
        /* Restore uncommitted enums. */
        uncommittedenumsspace = shm_toc_lookup(toc, 
PARALLEL_KEY_UNCOMMITTEDENUMS,
                                                                                
   false);
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index f7e7395c173..4e3b4494759 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -1509,6 +1509,30 @@ execute_extension_script(Oid extensionOid, 
ExtensionControlFile *control,
                SetUserIdAndSecContext(save_userid, save_sec_context);
 }
 
+/*
+ * GetExtensionCreationState - report the current extension-script state
+ *
+ * Used by the parallel-query machinery to carry creating_extension and
+ * CurrentExtensionObject to workers, so a worker sees the same
+ * extension-script state as the leader.
+ */
+void
+GetExtensionCreationState(bool *creating, Oid *extensionObject)
+{
+       *creating = creating_extension;
+       *extensionObject = CurrentExtensionObject;
+}
+
+/*
+ * SetExtensionCreationState - restore extension-script state in a worker
+ */
+void
+SetExtensionCreationState(bool creating, Oid extensionObject)
+{
+       creating_extension = creating;
+       CurrentExtensionObject = extensionObject;
+}
+
 /*
  * Find or create an ExtensionVersionInfo for the specified version name
  *
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..8eaec2d4f68 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -32,6 +32,9 @@ extern PGDLLIMPORT char *Extension_control_path;
 extern PGDLLIMPORT bool creating_extension;
 extern PGDLLIMPORT Oid CurrentExtensionObject;
 
+extern void GetExtensionCreationState(bool *creating, Oid *extensionObject);
+extern void SetExtensionCreationState(bool creating, Oid extensionObject);
+
 
 extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt 
*stmt);
 
-- 
2.47.3

From 8a0e1f1ba43218b1a8d3685170dc0fde58324792 Mon Sep 17 00:00:00 2001
From: Jan Nidzwetzki <[email protected]>
Date: Wed, 5 Aug 2026 15:21:09 +0200
Subject: [PATCH 2/2] Prefer trusted candidates when resolving names in
 extension scripts

An extension script runs with superuser privileges, so a user who can
create objects in the extension's schema can plant one that captures a
reference the script makes, f(text) beside the extension's f(varchar)
or a domain shadowing a required extension's, and run code with those
privileges.  Pinning search_path does not help: the plant is in the
script's own first schema.

Ignore untrusted objects while a script runs.  Trusted means in
pg_catalog, owned by a superuser, owned by the role running the script,
or a member of the extension being installed or of one it requires.  The
relation, type, function and operator lookups all apply the test, the
last two as they gather candidates so a plant cannot displace a trusted
match by search-path position.  Resolution otherwise fails as if the
object did not exist, with a detail saying why.  Cached plans record
whether they were analyzed inside a script, since a script's search_path
can match one the session already had.
---
 src/backend/catalog/namespace.c               | 210 +++++++++---
 src/backend/catalog/pg_operator.c             |  18 ++
 src/backend/commands/extension.c              |  51 +++
 src/backend/parser/parse_func.c               |  21 +-
 src/backend/parser/parse_oper.c               |  33 +-
 src/backend/utils/cache/plancache.c           |  29 ++
 src/include/catalog/namespace.h               |   4 +
 src/include/commands/extension.h              |   1 +
 src/include/utils/plancache.h                 |   1 +
 src/test/modules/test_extensions/Makefile     |  16 +
 .../expected/test_extensions.out              | 300 ++++++++++++++++++
 src/test/modules/test_extensions/meson.build  |  18 ++
 .../test_extensions/sql/test_extensions.sql   | 183 +++++++++++
 .../test_ext_overload--1.0.sql                |  19 ++
 .../test_extensions/test_ext_overload.control |   3 +
 .../test_ext_overload_nosuper--1.0--2.0.sql   |   9 +
 .../test_ext_overload_nosuper--1.0.sql        |  12 +
 .../test_ext_overload_nosuper.control         |   4 +
 .../test_ext_overload_parallel--1.0.sql       |  15 +
 .../test_ext_overload_parallel.control        |   3 +
 .../test_ext_overload_req--1.0.sql            |  20 ++
 .../test_ext_overload_req.control             |   4 +
 .../test_ext_overload_req_dep--1.0.sql        |  23 ++
 .../test_ext_overload_req_dep.control         |   4 +
 .../test_ext_overload_strict--1.0.sql         |   9 +
 .../test_ext_overload_strict--2.0.sql         |   9 +
 .../test_ext_overload_strict--3.0.sql         |  10 +
 .../test_ext_overload_strict--4.0.sql         |   9 +
 .../test_ext_overload_strict--5.0.sql         |   8 +
 .../test_ext_overload_strict--6.0.sql         |   8 +
 .../test_ext_overload_strict.control          |   3 +
 31 files changed, 1008 insertions(+), 49 deletions(-)
 create mode 100644 src/test/modules/test_extensions/test_ext_overload--1.0.sql
 create mode 100644 src/test/modules/test_extensions/test_ext_overload.control
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_nosuper--1.0--2.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_nosuper--1.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_nosuper.control
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_parallel--1.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_parallel.control
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_req--1.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_req.control
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_req_dep--1.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_req_dep.control
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--1.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--2.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--3.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--4.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--5.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict--6.0.sql
 create mode 100644 
src/test/modules/test_extensions/test_ext_overload_strict.control

diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 0647a198dea..737d03e939f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "commands/extension.h"
 #include "common/hashfn_unstable.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
@@ -225,6 +226,8 @@ static bool TSParserIsVisibleExt(Oid prsId, bool 
*is_missing);
 static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing);
 static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing);
 static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing);
+static bool RelationIsTrustedInExtensionScript(Oid relid);
+static bool TypeIsTrustedInExtensionScript(Oid typid);
 static void recomputeNamespacePath(void);
 static void AccessTempTableNamespace(bool force);
 static void InitTempTableNamespace(void);
@@ -896,13 +899,41 @@ RelnameGetRelid(const char *relname)
 
                relid = get_relname_relid(relname, namespaceId);
                if (OidIsValid(relid))
+               {
+                       /* Skip untrusted matches while an extension script 
runs */
+                       if (creating_extension &&
+                               !RelationIsTrustedInExtensionScript(relid))
+                               continue;
                        return relid;
+               }
        }
 
        /* Not found in path */
        return InvalidOid;
 }
 
+/*
+ * RelationIsTrustedInExtensionScript
+ *             ObjectIsTrustedInExtensionScript for a relation, by OID.
+ */
+static bool
+RelationIsTrustedInExtensionScript(Oid relid)
+{
+       HeapTuple       tp;
+       Form_pg_class form;
+       bool            result;
+
+       tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+       if (!HeapTupleIsValid(tp))
+               return true;
+       form = (Form_pg_class) GETSTRUCT(tp);
+       result = ObjectIsTrustedInExtensionScript(RelationRelationId, relid,
+                                                                               
          form->relnamespace,
+                                                                               
          form->relowner);
+       ReleaseSysCache(tp);
+       return result;
+}
+
 
 /*
  * RelationIsVisible
@@ -1024,13 +1055,40 @@ TypenameGetTypidExtended(const char *typname, bool 
temp_ok)
                                                                
PointerGetDatum(typname),
                                                                
ObjectIdGetDatum(namespaceId));
                if (OidIsValid(typid))
+               {
+                       /* Skip untrusted matches while an extension script 
runs */
+                       if (creating_extension && 
!TypeIsTrustedInExtensionScript(typid))
+                               continue;
                        return typid;
+               }
        }
 
        /* Not found in path */
        return InvalidOid;
 }
 
+/*
+ * TypeIsTrustedInExtensionScript
+ *             ObjectIsTrustedInExtensionScript for a type, by OID.
+ */
+static bool
+TypeIsTrustedInExtensionScript(Oid typid)
+{
+       HeapTuple       tp;
+       Form_pg_type form;
+       bool            result;
+
+       tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+       if (!HeapTupleIsValid(tp))
+               return true;
+       form = (Form_pg_type) GETSTRUCT(tp);
+       result = ObjectIsTrustedInExtensionScript(TypeRelationId, typid,
+                                                                               
          form->typnamespace,
+                                                                               
          form->typowner);
+       ReleaseSysCache(tp);
+       return result;
+}
+
 /*
  * TypeIsVisible
  *             Determine whether a type (identified by OID) is visible in the
@@ -1278,6 +1336,21 @@ FuncnameGetCandidates(List *names, int nargs, List 
*argnames,
                                continue;               /* proc is not in 
search path */
                }
 
+               /*
+                * During an extension script, skip untrusted candidates before 
any
+                * further flags are set, so the remaining flags describe 
trusted
+                * candidates only (see ObjectIsTrustedInExtensionScript).
+                */
+               if (creating_extension &&
+                       !ObjectIsTrustedInExtensionScript(ProcedureRelationId,
+                                                                               
          procform->oid,
+                                                                               
          procform->pronamespace,
+                                                                               
          procform->proowner))
+               {
+                       *fgc_flags |= FGC_UNTRUSTED_SKIP;
+                       continue;
+               }
+
                *fgc_flags |= FGC_NAME_VISIBLE; /* routine is in the right 
schema */
 
                /*
@@ -1591,6 +1664,35 @@ FuncnameGetCandidates(List *names, int nargs, List 
*argnames,
        return resultList;
 }
 
+/*
+ * ObjectIsTrustedInExtensionScript
+ *             May an extension script safely resolve a name to this object?
+ *
+ * Trusted means in pg_catalog, owned by a superuser, owned by the role running
+ * the script, or a member of the extension being installed or of one it
+ * requires.  The membership rule lets a script reach objects that an earlier
+ * version of itself, or a "superuser = false" required extension, created
+ * under some other role.
+ */
+bool
+ObjectIsTrustedInExtensionScript(Oid classId, Oid objectId,
+                                                                Oid 
namespaceId, Oid ownerId)
+{
+       Oid                     extensionId;
+
+       if (namespaceId == PG_CATALOG_NAMESPACE ||
+               superuser_arg(ownerId) ||
+               ownerId == GetUserId())
+               return true;
+
+       extensionId = getExtensionOfObject(classId, objectId);
+       if (!OidIsValid(extensionId))
+               return false;
+
+       return extensionId == CurrentExtensionObject ||
+               CurrentExtensionRequires(extensionId);
+}
+
 /*
  * MatchNamedCall
  *             Given a pg_proc heap tuple and a call's list of argument names,
@@ -1861,6 +1963,14 @@ OpernameGetOprid(List *names, Oid oprleft, Oid oprright)
                                Form_pg_operator operclass = (Form_pg_operator) 
GETSTRUCT(opertup);
                                Oid                     result = operclass->oid;
 
+                               /* Reject an untrusted match while an extension 
script runs */
+                               if (creating_extension &&
+                                       
!ObjectIsTrustedInExtensionScript(OperatorRelationId,
+                                                                               
                          result,
+                                                                               
                          operclass->oprnamespace,
+                                                                               
                          operclass->oprowner))
+                                       result = InvalidOid;
+
                                ReleaseSysCache(opertup);
                                return result;
                        }
@@ -1906,6 +2016,14 @@ OpernameGetOprid(List *names, Oid oprleft, Oid oprright)
                        {
                                Oid                     result = operform->oid;
 
+                               /* Skip untrusted matches while an extension 
script runs */
+                               if (creating_extension &&
+                                       
!ObjectIsTrustedInExtensionScript(OperatorRelationId,
+                                                                               
                          result,
+                                                                               
                          operform->oprnamespace,
+                                                                               
                          operform->oprowner))
+                                       continue;
+
                                ReleaseSysCacheList(catlist);
                                return result;
                        }
@@ -2033,53 +2151,63 @@ OpernameGetCandidates(List *names, char oprkind, bool 
missing_schema_ok,
                        }
                        if (nsp == NULL)
                                continue;               /* oper is not in 
search path */
+               }
 
-                       /*
-                        * Okay, it's in the search path, but does it have the 
same
-                        * arguments as something we already accepted?  If so, 
keep only
-                        * the one that appears earlier in the search path.
-                        *
-                        * If we have an ordered list from SearchSysCacheList 
(the normal
-                        * case), then any conflicting oper must immediately 
adjoin this
-                        * one in the list, so we only need to look at the 
newest result
-                        * item.  If we have an unordered list, we have to scan 
the whole
-                        * result list.
-                        */
-                       if (resultList)
-                       {
-                               FuncCandidateList prevResult;
+               /* Likewise skip untrusted candidates, as in 
FuncnameGetCandidates */
+               if (creating_extension &&
+                       !ObjectIsTrustedInExtensionScript(OperatorRelationId,
+                                                                               
          operform->oid,
+                                                                               
          operform->oprnamespace,
+                                                                               
          operform->oprowner))
+               {
+                       *fgc_flags |= FGC_UNTRUSTED_SKIP;
+                       continue;
+               }
 
-                               if (catlist->ordered)
-                               {
-                                       if (operform->oprleft == 
resultList->args[0] &&
-                                               operform->oprright == 
resultList->args[1])
-                                               prevResult = resultList;
-                                       else
-                                               prevResult = NULL;
-                               }
+               /*
+                * Okay, it's in the search path, but does it have the same 
arguments
+                * as something we already accepted?  If so, keep only the one 
that
+                * appears earlier in the search path.
+                *
+                * If we have an ordered list from SearchSysCacheList (the 
normal
+                * case), then any conflicting oper must immediately adjoin 
this one
+                * in the list, so we only need to look at the newest result 
item.  If
+                * we have an unordered list, we have to scan the whole result 
list.
+                */
+               if (!OidIsValid(namespaceId) && resultList)
+               {
+                       FuncCandidateList prevResult;
+
+                       if (catlist->ordered)
+                       {
+                               if (operform->oprleft == resultList->args[0] &&
+                                       operform->oprright == 
resultList->args[1])
+                                       prevResult = resultList;
                                else
+                                       prevResult = NULL;
+                       }
+                       else
+                       {
+                               for (prevResult = resultList;
+                                        prevResult;
+                                        prevResult = prevResult->next)
                                {
-                                       for (prevResult = resultList;
-                                                prevResult;
-                                                prevResult = prevResult->next)
-                                       {
-                                               if (operform->oprleft == 
prevResult->args[0] &&
-                                                       operform->oprright == 
prevResult->args[1])
-                                                       break;
-                                       }
-                               }
-                               if (prevResult)
-                               {
-                                       /* We have a match with a previous 
result */
-                                       Assert(pathpos != prevResult->pathpos);
-                                       if (pathpos > prevResult->pathpos)
-                                               continue;       /* keep 
previous result */
-                                       /* replace previous result */
-                                       prevResult->pathpos = pathpos;
-                                       prevResult->oid = operform->oid;
-                                       continue;       /* args are same, of 
course */
+                                       if (operform->oprleft == 
prevResult->args[0] &&
+                                               operform->oprright == 
prevResult->args[1])
+                                               break;
                                }
                        }
+                       if (prevResult)
+                       {
+                               /* We have a match with a previous result */
+                               Assert(pathpos != prevResult->pathpos);
+                               if (pathpos > prevResult->pathpos)
+                                       continue;       /* keep previous result 
*/
+                               /* replace previous result */
+                               prevResult->pathpos = pathpos;
+                               prevResult->oid = operform->oid;
+                               continue;               /* args are same, of 
course */
+                       }
                }
 
                *fgc_flags |= FGC_NAME_VISIBLE; /* operator is in the right 
schema */
diff --git a/src/backend/catalog/pg_operator.c 
b/src/backend/catalog/pg_operator.c
index 6b90c774c18..37c7d2e6c5c 100644
--- a/src/backend/catalog/pg_operator.c
+++ b/src/backend/catalog/pg_operator.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/extension.h"
 #include "miscadmin.h"
 #include "parser/parse_oper.h"
 #include "utils/acl.h"
@@ -643,6 +644,23 @@ get_other_operator(List *otherOp, Oid otherLeftTypeId, Oid 
otherRightTypeId,
        otherNamespace = QualifiedNameGetCreationNamespace(otherOp,
                                                                                
                           &otherName);
 
+       /*
+        * If the lookup failed only because the operator is untrusted during an
+        * extension script, say so rather than colliding with it below.
+        */
+       if (creating_extension &&
+               OidIsValid(OperatorGet(otherName, otherNamespace,
+                                                          otherLeftTypeId, 
otherRightTypeId,
+                                                          &otherDefined)))
+               ereport(ERROR,
+                               (errcode(ERRCODE_UNDEFINED_FUNCTION),
+                                errmsg("operator does not exist: %s",
+                                               op_signature_string(otherOp,
+                                                                               
        otherLeftTypeId,
+                                                                               
        otherRightTypeId)),
+                                errdetail("An operator of that name exists, 
but it is not trusted while an extension script runs."),
+                                errhint("Only objects in pg_catalog, owned by 
a superuser, or belonging to the extension or one it requires are trusted.")));
+
        if (strcmp(otherName, operatorName) == 0 &&
                otherNamespace == operatorNamespace &&
                otherLeftTypeId == leftTypeId &&
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 4e3b4494759..3618be78751 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -1533,6 +1533,57 @@ SetExtensionCreationState(bool creating, Oid 
extensionObject)
        CurrentExtensionObject = extensionObject;
 }
 
+/*
+ * CurrentExtensionRequires - does the running script's extension require this
+ * extension?
+ *
+ * Only direct requirements count; those are the ones whose schemas
+ * execute_extension_script puts into the script's search path.
+ */
+bool
+CurrentExtensionRequires(Oid extensionId)
+{
+       Relation        depRel;
+       ScanKeyData key[2];
+       SysScanDesc depScan;
+       HeapTuple       depTup;
+       bool            result = false;
+
+       if (!OidIsValid(CurrentExtensionObject))
+               return false;
+
+       depRel = table_open(DependRelationId, AccessShareLock);
+
+       ScanKeyInit(&key[0],
+                               Anum_pg_depend_classid,
+                               BTEqualStrategyNumber, F_OIDEQ,
+                               ObjectIdGetDatum(ExtensionRelationId));
+       ScanKeyInit(&key[1],
+                               Anum_pg_depend_objid,
+                               BTEqualStrategyNumber, F_OIDEQ,
+                               ObjectIdGetDatum(CurrentExtensionObject));
+
+       depScan = systable_beginscan(depRel, DependDependerIndexId, true,
+                                                                NULL, 2, key);
+
+       while (HeapTupleIsValid(depTup = systable_getnext(depScan)))
+       {
+               Form_pg_depend pg_depend = (Form_pg_depend) GETSTRUCT(depTup);
+
+               if (pg_depend->refclassid == ExtensionRelationId &&
+                       pg_depend->refobjid == extensionId)
+               {
+                       result = true;
+                       break;
+               }
+       }
+
+       systable_endscan(depScan);
+       table_close(depRel, AccessShareLock);
+
+       return result;
+}
+
 /*
  * Find or create an ExtensionVersionInfo for the specified version name
  *
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index c87804f5d41..f5ec5ffa516 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -1003,7 +1003,15 @@ func_lookup_failure_details(int fgc_flags, List 
*argnames, bool proc_call)
         */
        if (!(fgc_flags & FGC_NAME_VISIBLE))
        {
-               if (fgc_flags & FGC_SCHEMA_GIVEN)
+               if (fgc_flags & FGC_UNTRUSTED_SKIP)
+               {
+                       if (proc_call)
+                               (void) errdetail("A procedure of that name 
exists, but it is not trusted while an extension script runs.");
+                       else
+                               (void) errdetail("A function of that name 
exists, but it is not trusted while an extension script runs.");
+                       return errhint("Only objects in pg_catalog, owned by a 
superuser, or belonging to the extension or one it requires are trusted.");
+               }
+               else if (fgc_flags & FGC_SCHEMA_GIVEN)
                        return 0;                       /* schema-qualified 
name */
                else if (!(fgc_flags & FGC_NAME_EXISTS))
                {
@@ -1021,6 +1029,15 @@ func_lookup_failure_details(int fgc_flags, List 
*argnames, bool proc_call)
                }
        }
 
+       /* A trusted candidate was visible; mention any skipped one as a hint */
+       if (fgc_flags & FGC_UNTRUSTED_SKIP)
+       {
+               if (proc_call)
+                       (void) errhint("A procedure of that name was ignored 
because it is not trusted while an extension script runs.");
+               else
+                       (void) errhint("A function of that name was ignored 
because it is not trusted while an extension script runs.");
+       }
+
        /*
         * Next, complain if nothing had the right number of arguments.  (This
         * takes precedence over wrong-argnames cases because we won't even look
@@ -1076,6 +1093,8 @@ func_lookup_failure_details(int fgc_flags, List 
*argnames, bool proc_call)
                (void) errdetail("No procedure of that name accepts the given 
argument types.");
        else
                (void) errdetail("No function of that name accepts the given 
argument types.");
+       if (fgc_flags & FGC_UNTRUSTED_SKIP)
+               return 0;                               /* keep the hint set 
above */
        return errhint("You might need to add explicit type casts.");
 }
 
diff --git a/src/backend/parser/parse_oper.c b/src/backend/parser/parse_oper.c
index dc0f047ca25..a97422786d3 100644
--- a/src/backend/parser/parse_oper.c
+++ b/src/backend/parser/parse_oper.c
@@ -16,8 +16,10 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_type.h"
+#include "commands/extension.h"
 #include "lib/stringinfo.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_coerce.h"
@@ -388,6 +390,13 @@ oper(ParseState *pstate, List *opname, Oid ltypeId, Oid 
rtypeId,
         */
        key_ok = make_oper_cache_key(pstate, &key, opname, ltypeId, rtypeId, 
location);
 
+       /*
+        * Skip the lookaside cache during an extension script, so the trust
+        * checks below see the catalog state.
+        */
+       if (creating_extension)
+               key_ok = false;
+
        if (key_ok)
        {
                operOid = find_oper_cache_entry(&key);
@@ -540,6 +549,10 @@ left_oper(ParseState *pstate, List *op, Oid arg, bool 
noError, int location)
         */
        key_ok = make_oper_cache_key(pstate, &key, op, InvalidOid, arg, 
location);
 
+       /* Skip the lookaside cache during an extension script; see oper() */
+       if (creating_extension)
+               key_ok = false;
+
        if (key_ok)
        {
                operOid = find_oper_cache_entry(&key);
@@ -672,7 +685,12 @@ oper_lookup_failure_details(int fgc_flags, bool 
is_unary_op)
         */
        if (!(fgc_flags & FGC_NAME_VISIBLE))
        {
-               if (fgc_flags & FGC_SCHEMA_GIVEN)
+               if (fgc_flags & FGC_UNTRUSTED_SKIP)
+               {
+                       (void) errdetail("An operator of that name exists, but 
it is not trusted while an extension script runs.");
+                       return errhint("Only objects in pg_catalog, owned by a 
superuser, or belonging to the extension or one it requires are trusted.");
+               }
+               else if (fgc_flags & FGC_SCHEMA_GIVEN)
                        return 0;                       /* schema-qualified 
name */
                else if (!(fgc_flags & FGC_NAME_EXISTS))
                        return errdetail("There is no operator of that name.");
@@ -681,18 +699,19 @@ oper_lookup_failure_details(int fgc_flags, bool 
is_unary_op)
        }
 
        /*
-        * Otherwise, the problem must be incorrect argument type(s).
+        * Otherwise, the problem must be incorrect argument type(s); mention 
any
+        * skipped untrusted candidate in place of the usual hint.
         */
        if (is_unary_op)
-       {
                (void) errdetail("No operator of that name accepts the given 
argument type.");
-               return errhint("You might need to add an explicit type cast.");
-       }
        else
-       {
                (void) errdetail("No operator of that name accepts the given 
argument types.");
+       if (fgc_flags & FGC_UNTRUSTED_SKIP)
+               return errhint("An operator of that name was ignored because it 
is not trusted while an extension script runs.");
+       else if (is_unary_op)
+               return errhint("You might need to add an explicit type cast.");
+       else
                return errhint("You might need to add explicit type casts.");
-       }
 }
 
 /*
diff --git a/src/backend/utils/cache/plancache.c 
b/src/backend/utils/cache/plancache.c
index fb3b38ffbbf..8b4d5e5990b 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -61,6 +61,7 @@
 
 #include "access/transam.h"
 #include "catalog/namespace.h"
+#include "commands/extension.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -243,6 +244,7 @@ CreateCachedPlan(const RawStmt *raw_parse_tree,
        plansource->rewriteRoleId = InvalidOid;
        plansource->rewriteRowSecurity = false;
        plansource->dependsOnRLS = false;
+       plansource->parsedInExtensionScript = false;
        plansource->gplan = NULL;
        plansource->is_oneshot = false;
        plansource->is_complete = false;
@@ -342,6 +344,7 @@ CreateOneShotCachedPlan(RawStmt *raw_parse_tree,
        plansource->rewriteRoleId = InvalidOid;
        plansource->rewriteRowSecurity = false;
        plansource->dependsOnRLS = false;
+       plansource->parsedInExtensionScript = false;
        plansource->gplan = NULL;
        plansource->is_oneshot = true;
        plansource->is_complete = false;
@@ -462,6 +465,9 @@ CompleteCachedPlan(CachedPlanSource *plansource,
                plansource->rewriteRoleId = GetUserId();
                plansource->rewriteRowSecurity = row_security;
 
+               /* Remember whether an extension script was running. */
+               plansource->parsedInExtensionScript = creating_extension;
+
                /*
                 * Also save the current search_path in the query_context.  
(This
                 * should not generate much extra cruft either, since almost 
certainly
@@ -733,6 +739,20 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
                }
        }
 
+       /*
+        * Name resolution applies extra trust checks while an extension script
+        * runs, so a tree analyzed outside one must not be reused inside it or
+        * vice versa.  The search_path check above need not have caught this.
+        */
+       if (plansource->is_valid &&
+               plansource->parsedInExtensionScript != creating_extension)
+       {
+               /* Invalidate the querytree and generic plan */
+               plansource->is_valid = false;
+               if (plansource->gplan)
+                       plansource->gplan->is_valid = false;
+       }
+
        /*
         * If the query rewrite phase had a possible RLS dependency, we must 
redo
         * it if either the role or the row_security setting has changed.
@@ -918,6 +938,9 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
        plansource->rewriteRoleId = GetUserId();
        plansource->rewriteRowSecurity = row_security;
 
+       /* Remember whether an extension script was running. */
+       plansource->parsedInExtensionScript = creating_extension;
+
        /*
         * Also save the current search_path in the query_context.  (This should
         * not generate much extra cruft either, since almost certainly the path
@@ -1498,6 +1521,7 @@ CachedPlanAllowsSimpleValidityCheck(CachedPlanSource 
*plansource,
        Assert(plan == plansource->gplan);
        Assert(plansource->search_path != NULL);
        Assert(SearchPathMatchesCurrentEnvironment(plansource->search_path));
+       Assert(plansource->parsedInExtensionScript == creating_extension);
 
        /* We don't support oneshot plans here. */
        if (plansource->is_oneshot)
@@ -1623,6 +1647,10 @@ CachedPlanIsSimplyValid(CachedPlanSource *plansource, 
CachedPlan *plan,
        if (!SearchPathMatchesCurrentEnvironment(plansource->search_path))
                return false;
 
+       /* Are we in the same extension-script context as when we made it? */
+       if (plansource->parsedInExtensionScript != creating_extension)
+               return false;
+
        /* It's still good.  Bump refcount if requested. */
        if (owner)
        {
@@ -1743,6 +1771,7 @@ CopyCachedPlan(CachedPlanSource *plansource)
        newsource->rewriteRoleId = plansource->rewriteRoleId;
        newsource->rewriteRowSecurity = plansource->rewriteRowSecurity;
        newsource->dependsOnRLS = plansource->dependsOnRLS;
+       newsource->parsedInExtensionScript = 
plansource->parsedInExtensionScript;
 
        newsource->gplan = NULL;
 
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 9453a3e4932..be8410c3e99 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -56,6 +56,8 @@ typedef struct _FuncCandidateList
 #define FGC_ARGNAMES_VALID     0x0100  /* Found a fully-valid use of argnames 
*/
 /* These bits are actually filled by func_get_detail: */
 #define FGC_VARIADIC_FAIL      0x0200  /* Disallowed VARIADIC with named args 
*/
+/* This bit is set only while an extension script is running: */
+#define FGC_UNTRUSTED_SKIP     0x0400  /* Ignored an untrusted candidate */
 
 /*
  * Result of checkTempNamespaceStatus
@@ -122,6 +124,8 @@ extern FuncCandidateList FuncnameGetCandidates(List *names,
                                                                                
           bool include_out_arguments,
                                                                                
           bool missing_ok,
                                                                                
           int *fgc_flags);
+extern bool ObjectIsTrustedInExtensionScript(Oid classId, Oid objectId,
+                                                                               
         Oid namespaceId, Oid ownerId);
 extern bool FunctionIsVisible(Oid funcid);
 
 extern Oid     OpernameGetOprid(List *names, Oid oprleft, Oid oprright);
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 8eaec2d4f68..327502f9311 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -34,6 +34,7 @@ extern PGDLLIMPORT Oid CurrentExtensionObject;
 
 extern void GetExtensionCreationState(bool *creating, Oid *extensionObject);
 extern void SetExtensionCreationState(bool creating, Oid extensionObject);
+extern bool CurrentExtensionRequires(Oid extensionId);
 
 
 extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt 
*stmt);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a0355e79c28..042ea128866 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -129,6 +129,7 @@ typedef struct CachedPlanSource
        Oid                     rewriteRoleId;  /* Role ID we did rewriting for 
*/
        bool            rewriteRowSecurity; /* row_security used during rewrite 
*/
        bool            dependsOnRLS;   /* is rewritten query specific to the 
above? */
+       bool            parsedInExtensionScript;        /* creating_extension 
at parse */
        /* If we have a generic plan, this is a reference-counted link to it: */
        struct CachedPlan *gplan;       /* generic plan, or NULL if not valid */
        /* Some state flags: */
diff --git a/src/test/modules/test_extensions/Makefile 
b/src/test/modules/test_extensions/Makefile
index d1b0b81e5fd..71e489eaf58 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,6 +9,10 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 
test_ext6 \
             test_ext_cyclic1 test_ext_cyclic2 \
             test_ext_extschema \
             test_ext_evttrig \
+            test_ext_overload test_ext_overload_strict \
+            test_ext_overload_nosuper \
+            test_ext_overload_parallel \
+            test_ext_overload_req test_ext_overload_req_dep \
             test_ext_set_schema \
             test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
 
@@ -25,6 +29,18 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql 
test_ext3--1.0.sql \
        test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \
        test_ext_extschema--1.0.sql \
        test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \
+       test_ext_overload--1.0.sql \
+       test_ext_overload_strict--1.0.sql \
+       test_ext_overload_strict--2.0.sql \
+       test_ext_overload_strict--3.0.sql \
+       test_ext_overload_strict--4.0.sql \
+       test_ext_overload_strict--5.0.sql \
+       test_ext_overload_strict--6.0.sql \
+       test_ext_overload_nosuper--1.0.sql \
+       test_ext_overload_nosuper--1.0--2.0.sql \
+       test_ext_overload_parallel--1.0.sql \
+       test_ext_overload_req--1.0.sql \
+       test_ext_overload_req_dep--1.0.sql \
        test_ext_set_schema--1.0.sql \
        test_ext_req_schema1--1.0.sql \
        test_ext_req_schema2--1.0.sql \
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out 
b/src/test/modules/test_extensions/expected/test_extensions.out
index 1b5debdeeb1..b44f7aade1b 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -667,3 +667,303 @@ SELECT test_s_dep.dep_req2();
 
 DROP EXTENSION test_ext_req_schema1 CASCADE;
 NOTICE:  drop cascades to extension test_ext_req_schema2
+-- Verify that name resolution during an extension script cannot be captured
+-- by objects an unprivileged user planted in the extension's schema.
+CREATE ROLE regress_ext_user;
+CREATE SCHEMA test_overload;
+GRANT CREATE, USAGE ON SCHEMA test_overload TO regress_ext_user;
+-- As the unprivileged user, plant differently-typed siblings and the sole
+-- definition of helper_only().
+SET ROLE regress_ext_user;
+CREATE FUNCTION test_overload.f(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_overload.opimpl_bad(text, text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.### (leftarg = text, rightarg = text,
+                                   function = test_overload.opimpl_bad);
+CREATE FUNCTION test_overload.helper_only(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_overload.opimpl_only(text, text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.@@@ (leftarg = text, rightarg = text,
+                                   function = test_overload.opimpl_only);
+CREATE FUNCTION test_overload.opimpl_vc(varchar, varchar) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.<<< (leftarg = varchar, rightarg = varchar,
+                                   function = test_overload.opimpl_vc);
+RESET ROLE;
+-- A schema outside the script's search path, holding a planted operator.
+CREATE SCHEMA test_overload_other;
+GRANT CREATE, USAGE ON SCHEMA test_overload_other TO regress_ext_user;
+SET ROLE regress_ext_user;
+CREATE OPERATOR test_overload_other.&&& (leftarg = text, rightarg = text,
+                                         function = test_overload.opimpl_only);
+RESET ROLE;
+-- Installing the extension resolves f('abc') and the ### operator to the
+-- extension's own (trusted) objects, not the planted ones.
+CREATE EXTENSION test_ext_overload SCHEMA test_overload;
+SELECT fn, op FROM test_overload.captured;
+    fn     |    op     
+-----------+-----------
+ extension | extension
+(1 row)
+
+-- Outside of extension scripts, ordinary resolution rules are unchanged: the
+-- same calls reach the planted objects.
+SELECT test_overload.f('abc') AS fn,
+       ('a' OPERATOR(test_overload.###) 'b') AS op;
+    fn    |    op    
+----------+----------
+ attacker | attacker
+(1 row)
+
+-- When only an untrusted candidate exists, the script refuses to call it.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload;  -- fails
+ERROR:  function test_overload.helper_only(unknown) does not exist
+LINE 2:     SELECT test_overload.helper_only('abc') AS r
+                   ^
+DETAIL:  A function of that name exists, but it is not trusted while an 
extension script runs.
+HINT:  Only objects in pg_catalog, owned by a superuser, or belonging to the 
extension or one it requires are trusted.
+QUERY:  CREATE TABLE test_overload.captured AS
+    SELECT test_overload.helper_only('abc') AS r
+CONTEXT:  extension script file "test_ext_overload_strict--1.0.sql", near line 
8
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '2.0';  
-- fails
+ERROR:  operator does not exist: unknown test_overload.@@@ unknown
+LINE 2:     SELECT ('a' OPERATOR(test_overload.@@@) 'b') AS r
+                        ^
+DETAIL:  An operator of that name exists, but it is not trusted while an 
extension script runs.
+HINT:  Only objects in pg_catalog, owned by a superuser, or belonging to the 
extension or one it requires are trusted.
+QUERY:  CREATE TABLE test_overload.captured AS
+    SELECT ('a' OPERATOR(test_overload.@@@) 'b') AS r
+CONTEXT:  extension script file "test_ext_overload_strict--2.0.sql", near line 
8
+-- A planted operator named as COMMUTATOR is refused as well.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '3.0';  
-- fails
+ERROR:  operator does not exist: character varying <<< character varying
+DETAIL:  An operator of that name exists, but it is not trusted while an 
extension script runs.
+HINT:  Only objects in pg_catalog, owned by a superuser, or belonging to the 
extension or one it requires are trusted.
+CONTEXT:  SQL statement "CREATE OPERATOR test_overload.>>> (leftarg = varchar, 
rightarg = varchar,
+                                 function = test_overload.opimpl,
+                                 commutator = <<<)"
+extension script file "test_ext_overload_strict--3.0.sql", near line 8
+-- Trusted candidate visible but arguments don't match: argument error, with
+-- the untrusted candidate as a hint.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '4.0';  
-- fails
+ERROR:  function test_overload.f(integer) does not exist
+LINE 2:     SELECT test_overload.f(1) AS r
+                   ^
+DETAIL:  No function of that name accepts the given argument types.
+HINT:  A function of that name was ignored because it is not trusted while an 
extension script runs.
+QUERY:  CREATE TABLE test_overload.captured AS
+    SELECT test_overload.f(1) AS r
+CONTEXT:  extension script file "test_ext_overload_strict--4.0.sql", near line 
8
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '5.0';  
-- fails
+ERROR:  operator does not exist: integer test_overload.### integer
+LINE 2:     SELECT (1 OPERATOR(test_overload.###) 2) AS r
+                      ^
+DETAIL:  No operator of that name accepts the given argument types.
+HINT:  An operator of that name was ignored because it is not trusted while an 
extension script runs.
+QUERY:  CREATE TABLE test_overload.captured AS
+    SELECT (1 OPERATOR(test_overload.###) 2) AS r
+CONTEXT:  extension script file "test_ext_overload_strict--5.0.sql", near line 
7
+-- Untrusted candidate outside the search path: ordinary not-in-path error.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '6.0';  
-- fails
+ERROR:  operator does not exist: unknown &&& unknown
+LINE 2:     SELECT ('a' &&& 'b') AS r
+                        ^
+DETAIL:  An operator of that name exists, but it is not in the search_path.
+QUERY:  CREATE TABLE test_overload.captured AS
+    SELECT ('a' &&& 'b') AS r
+CONTEXT:  extension script file "test_ext_overload_strict--6.0.sql", near line 
7
+DROP EXTENSION test_ext_overload;
+DROP SCHEMA test_overload CASCADE;
+NOTICE:  drop cascades to 9 other objects
+DETAIL:  drop cascades to function test_overload.f(text)
+drop cascades to function test_overload.opimpl_bad(text,text)
+drop cascades to operator test_overload.###(text,text)
+drop cascades to function test_overload.helper_only(text)
+drop cascades to function test_overload.opimpl_only(text,text)
+drop cascades to operator test_overload_other.&&&(text,text)
+drop cascades to operator test_overload.@@@(text,text)
+drop cascades to function test_overload.opimpl_vc(character varying,character 
varying)
+drop cascades to operator test_overload.<<<(character varying,character 
varying)
+DROP SCHEMA test_overload_other CASCADE;
+DROP ROLE regress_ext_user;
+-- A "superuser = false" script runs as the invoking user, so its objects are
+-- owned by that role.  They must still be trusted, and another user's plant
+-- must not be.
+CREATE ROLE regress_ext_owner;
+CREATE ROLE regress_ext_attacker;
+DO $$ BEGIN
+    EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_ext_owner',
+                   current_database());
+END $$;
+CREATE SCHEMA test_nosuper AUTHORIZATION regress_ext_owner;
+GRANT CREATE, USAGE ON SCHEMA test_nosuper TO regress_ext_attacker;
+-- Attacker plants a preferred-type (text) sibling of the extension's g().
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_nosuper.g(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+RESET ROLE;
+-- The script runs as regress_ext_owner and must resolve g('abc') to its own
+-- varchar function, not the attacker's text one.
+SET ROLE regress_ext_owner;
+CREATE EXTENSION test_ext_overload_nosuper SCHEMA test_nosuper;
+SELECT fn FROM test_nosuper.captured_nosuper;
+    fn     
+-----------
+ extension
+(1 row)
+
+RESET ROLE;
+-- An update run by another role (here the superuser) must still reach the
+-- extension's own g(), which the install left owned by regress_ext_owner.
+ALTER EXTENSION test_ext_overload_nosuper UPDATE TO '2.0';
+SELECT fn FROM test_nosuper.updated;
+    fn     
+-----------
+ extension
+(1 row)
+
+DROP EXTENSION test_ext_overload_nosuper;
+DROP SCHEMA test_nosuper CASCADE;
+NOTICE:  drop cascades to function test_nosuper.g(text)
+DO $$ BEGIN
+    EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_ext_owner',
+                   current_database());
+END $$;
+DROP ROLE regress_ext_owner;
+DROP ROLE regress_ext_attacker;
+-- Resolution in a parallel worker must apply the same check, so
+-- creating_extension must reach the worker.  Force wrap() into a worker with
+-- debug_parallel_query.
+CREATE ROLE regress_ext_attacker NOSUPERUSER;
+CREATE SCHEMA test_parallel;
+GRANT CREATE, USAGE ON SCHEMA test_parallel TO regress_ext_attacker;
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_parallel.probe(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
+RESET ROLE;
+SET debug_parallel_query = on;
+CREATE EXTENSION test_ext_overload_parallel SCHEMA test_parallel;
+RESET debug_parallel_query;
+-- The worker resolved probe('x') to the extension's own probe(varchar), not
+-- the planted probe(text).
+SELECT who FROM test_parallel.captured;
+    who    
+-----------
+ extension
+(1 row)
+
+DROP EXTENSION test_ext_overload_parallel;
+DROP SCHEMA test_parallel CASCADE;
+NOTICE:  drop cascades to function test_parallel.probe(text)
+DROP ROLE regress_ext_attacker;
+-- A plant in the extension's own schema must not shadow a required
+-- extension's object, for any kind of reference: call, DDL by name, type,
+-- relation, or a resolution cached before the script.
+CREATE ROLE regress_ext_attacker;
+CREATE ROLE regress_ext_reqowner;
+DO $$ BEGIN
+    EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_ext_reqowner',
+                   current_database());
+END $$;
+CREATE SCHEMA test_reqdep AUTHORIZATION regress_ext_reqowner;
+CREATE SCHEMA test_req;
+GRANT CREATE, USAGE ON SCHEMA test_req TO regress_ext_attacker;
+-- The required extension is "superuser = false" and installed by an ordinary
+-- role, so its objects are reachable only through required-extension
+-- membership.
+SET ROLE regress_ext_reqowner;
+CREATE EXTENSION test_ext_overload_req_dep SCHEMA test_reqdep;
+RESET ROLE;
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_req.reqcall(int) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_req.reqeq(int, int) RETURNS boolean
+    AS $$ SELECT false $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_req.=== (leftarg = integer, rightarg = integer,
+                              function = test_req.reqeq);
+-- Resolving to this domain would run pwn() with the script's privileges.
+CREATE FUNCTION test_req.pwn(text) RETURNS boolean
+    AS $$ BEGIN RAISE EXCEPTION 'attacker code executed'; END $$ LANGUAGE 
plpgsql;
+CREATE DOMAIN test_req.reqdom AS text CHECK (test_req.pwn(VALUE));
+CREATE TABLE test_req.reqtab(t text);
+RESET ROLE;
+-- Cache a resolution made outside any script, under the search_path the
+-- script will pin; that plan must not be reused inside the script.
+SET search_path = test_req, test_reqdep, pg_temp;
+SELECT test_reqdep.reqplpgsql() AS warmed_outside_script;
+ warmed_outside_script 
+-----------------------
+ attacker
+(1 row)
+
+RESET search_path;
+CREATE EXTENSION test_ext_overload_req SCHEMA test_req;
+-- Every reference resolved to the required extension's objects (in
+-- test_reqdep), not the planted ones in test_req.
+SELECT c.who, c.who_cached, n.nspname AS domain_schema
+  FROM test_req.captured c
+  JOIN pg_type t ON t.oid = c.dom
+  JOIN pg_namespace n ON n.oid = t.typnamespace;
+   who    | who_cached | domain_schema 
+----------+------------+---------------
+ required | required   | test_reqdep
+(1 row)
+
+SELECT n.nspname AS operator_func_schema
+  FROM pg_operator o
+  JOIN pg_proc p ON p.oid = o.oprcode
+  JOIN pg_namespace n ON n.oid = p.pronamespace
+ WHERE o.oprname = '###' AND o.oprnamespace = 'test_req'::regnamespace;
+ operator_func_schema 
+----------------------
+ test_reqdep
+(1 row)
+
+SELECT 'test_reqdep.reqtab' AS tbl, count(*) FROM test_reqdep.reqtab
+UNION ALL
+SELECT 'test_req.reqtab', count(*) FROM test_req.reqtab;
+        tbl         | count 
+--------------------+-------
+ test_reqdep.reqtab |     1
+ test_req.reqtab    |     0
+(2 rows)
+
+SELECT n.nspname AS opfamily_member_schema
+  FROM pg_amop a
+  JOIN pg_opfamily f ON f.oid = a.amopfamily
+  JOIN pg_operator o ON o.oid = a.amopopr
+  JOIN pg_namespace n ON n.oid = o.oprnamespace
+ WHERE f.opfname = 'reqfam';
+ opfamily_member_schema 
+------------------------
+ test_reqdep
+(1 row)
+
+-- Outside the script the cached plan is good again.
+SET search_path = test_req, test_reqdep, pg_temp;
+SELECT test_reqdep.reqplpgsql() AS after_script;
+ after_script 
+--------------
+ attacker
+(1 row)
+
+RESET search_path;
+DROP EXTENSION test_ext_overload_req;
+DROP EXTENSION test_ext_overload_req_dep;
+DROP SCHEMA test_req CASCADE;
+NOTICE:  drop cascades to 6 other objects
+DETAIL:  drop cascades to function test_req.reqcall(integer)
+drop cascades to function test_req.reqeq(integer,integer)
+drop cascades to operator test_req.===(integer,integer)
+drop cascades to function test_req.pwn(text)
+drop cascades to type test_req.reqdom
+drop cascades to table test_req.reqtab
+DROP SCHEMA test_reqdep CASCADE;
+DO $$ BEGIN
+    EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_ext_reqowner',
+                   current_database());
+END $$;
+DROP ROLE regress_ext_attacker;
+DROP ROLE regress_ext_reqowner;
diff --git a/src/test/modules/test_extensions/meson.build 
b/src/test/modules/test_extensions/meson.build
index 2c7cea189e2..8aeca48f32e 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -36,6 +36,24 @@ test_install_data += files(
   'test_ext_evttrig--1.0--2.0.sql',
   'test_ext_evttrig--1.0.sql',
   'test_ext_evttrig.control',
+  'test_ext_overload--1.0.sql',
+  'test_ext_overload.control',
+  'test_ext_overload_strict--1.0.sql',
+  'test_ext_overload_strict--2.0.sql',
+  'test_ext_overload_strict--3.0.sql',
+  'test_ext_overload_strict--4.0.sql',
+  'test_ext_overload_strict--5.0.sql',
+  'test_ext_overload_strict--6.0.sql',
+  'test_ext_overload_strict.control',
+  'test_ext_overload_nosuper--1.0--2.0.sql',
+  'test_ext_overload_nosuper--1.0.sql',
+  'test_ext_overload_nosuper.control',
+  'test_ext_overload_parallel--1.0.sql',
+  'test_ext_overload_parallel.control',
+  'test_ext_overload_req--1.0.sql',
+  'test_ext_overload_req.control',
+  'test_ext_overload_req_dep--1.0.sql',
+  'test_ext_overload_req_dep.control',
   'test_ext_req_schema1--1.0.sql',
   'test_ext_req_schema1.control',
   'test_ext_req_schema2--1.0.sql',
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql 
b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..1a09e98c115 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,186 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA 
test_s_dep2;  -- now ok
 SELECT test_s_dep2.dep_req1();
 SELECT test_s_dep.dep_req2();
 DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+-- Verify that name resolution during an extension script cannot be captured
+-- by objects an unprivileged user planted in the extension's schema.
+CREATE ROLE regress_ext_user;
+CREATE SCHEMA test_overload;
+GRANT CREATE, USAGE ON SCHEMA test_overload TO regress_ext_user;
+-- As the unprivileged user, plant differently-typed siblings and the sole
+-- definition of helper_only().
+SET ROLE regress_ext_user;
+CREATE FUNCTION test_overload.f(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_overload.opimpl_bad(text, text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.### (leftarg = text, rightarg = text,
+                                   function = test_overload.opimpl_bad);
+CREATE FUNCTION test_overload.helper_only(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_overload.opimpl_only(text, text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.@@@ (leftarg = text, rightarg = text,
+                                   function = test_overload.opimpl_only);
+CREATE FUNCTION test_overload.opimpl_vc(varchar, varchar) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_overload.<<< (leftarg = varchar, rightarg = varchar,
+                                   function = test_overload.opimpl_vc);
+RESET ROLE;
+-- A schema outside the script's search path, holding a planted operator.
+CREATE SCHEMA test_overload_other;
+GRANT CREATE, USAGE ON SCHEMA test_overload_other TO regress_ext_user;
+SET ROLE regress_ext_user;
+CREATE OPERATOR test_overload_other.&&& (leftarg = text, rightarg = text,
+                                         function = test_overload.opimpl_only);
+RESET ROLE;
+-- Installing the extension resolves f('abc') and the ### operator to the
+-- extension's own (trusted) objects, not the planted ones.
+CREATE EXTENSION test_ext_overload SCHEMA test_overload;
+SELECT fn, op FROM test_overload.captured;
+-- Outside of extension scripts, ordinary resolution rules are unchanged: the
+-- same calls reach the planted objects.
+SELECT test_overload.f('abc') AS fn,
+       ('a' OPERATOR(test_overload.###) 'b') AS op;
+-- When only an untrusted candidate exists, the script refuses to call it.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload;  -- fails
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '2.0';  
-- fails
+-- A planted operator named as COMMUTATOR is refused as well.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '3.0';  
-- fails
+-- Trusted candidate visible but arguments don't match: argument error, with
+-- the untrusted candidate as a hint.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '4.0';  
-- fails
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '5.0';  
-- fails
+-- Untrusted candidate outside the search path: ordinary not-in-path error.
+CREATE EXTENSION test_ext_overload_strict SCHEMA test_overload VERSION '6.0';  
-- fails
+DROP EXTENSION test_ext_overload;
+DROP SCHEMA test_overload CASCADE;
+DROP SCHEMA test_overload_other CASCADE;
+DROP ROLE regress_ext_user;
+
+-- A "superuser = false" script runs as the invoking user, so its objects are
+-- owned by that role.  They must still be trusted, and another user's plant
+-- must not be.
+CREATE ROLE regress_ext_owner;
+CREATE ROLE regress_ext_attacker;
+DO $$ BEGIN
+    EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_ext_owner',
+                   current_database());
+END $$;
+CREATE SCHEMA test_nosuper AUTHORIZATION regress_ext_owner;
+GRANT CREATE, USAGE ON SCHEMA test_nosuper TO regress_ext_attacker;
+-- Attacker plants a preferred-type (text) sibling of the extension's g().
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_nosuper.g(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+RESET ROLE;
+-- The script runs as regress_ext_owner and must resolve g('abc') to its own
+-- varchar function, not the attacker's text one.
+SET ROLE regress_ext_owner;
+CREATE EXTENSION test_ext_overload_nosuper SCHEMA test_nosuper;
+SELECT fn FROM test_nosuper.captured_nosuper;
+RESET ROLE;
+-- An update run by another role (here the superuser) must still reach the
+-- extension's own g(), which the install left owned by regress_ext_owner.
+ALTER EXTENSION test_ext_overload_nosuper UPDATE TO '2.0';
+SELECT fn FROM test_nosuper.updated;
+DROP EXTENSION test_ext_overload_nosuper;
+DROP SCHEMA test_nosuper CASCADE;
+DO $$ BEGIN
+    EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_ext_owner',
+                   current_database());
+END $$;
+DROP ROLE regress_ext_owner;
+DROP ROLE regress_ext_attacker;
+
+-- Resolution in a parallel worker must apply the same check, so
+-- creating_extension must reach the worker.  Force wrap() into a worker with
+-- debug_parallel_query.
+CREATE ROLE regress_ext_attacker NOSUPERUSER;
+CREATE SCHEMA test_parallel;
+GRANT CREATE, USAGE ON SCHEMA test_parallel TO regress_ext_attacker;
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_parallel.probe(text) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
+RESET ROLE;
+SET debug_parallel_query = on;
+CREATE EXTENSION test_ext_overload_parallel SCHEMA test_parallel;
+RESET debug_parallel_query;
+-- The worker resolved probe('x') to the extension's own probe(varchar), not
+-- the planted probe(text).
+SELECT who FROM test_parallel.captured;
+DROP EXTENSION test_ext_overload_parallel;
+DROP SCHEMA test_parallel CASCADE;
+DROP ROLE regress_ext_attacker;
+
+-- A plant in the extension's own schema must not shadow a required
+-- extension's object, for any kind of reference: call, DDL by name, type,
+-- relation, or a resolution cached before the script.
+CREATE ROLE regress_ext_attacker;
+CREATE ROLE regress_ext_reqowner;
+DO $$ BEGIN
+    EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_ext_reqowner',
+                   current_database());
+END $$;
+CREATE SCHEMA test_reqdep AUTHORIZATION regress_ext_reqowner;
+CREATE SCHEMA test_req;
+GRANT CREATE, USAGE ON SCHEMA test_req TO regress_ext_attacker;
+-- The required extension is "superuser = false" and installed by an ordinary
+-- role, so its objects are reachable only through required-extension
+-- membership.
+SET ROLE regress_ext_reqowner;
+CREATE EXTENSION test_ext_overload_req_dep SCHEMA test_reqdep;
+RESET ROLE;
+SET ROLE regress_ext_attacker;
+CREATE FUNCTION test_req.reqcall(int) RETURNS text
+    AS $$ SELECT 'attacker'::text $$ LANGUAGE sql IMMUTABLE;
+CREATE FUNCTION test_req.reqeq(int, int) RETURNS boolean
+    AS $$ SELECT false $$ LANGUAGE sql IMMUTABLE;
+CREATE OPERATOR test_req.=== (leftarg = integer, rightarg = integer,
+                              function = test_req.reqeq);
+-- Resolving to this domain would run pwn() with the script's privileges.
+CREATE FUNCTION test_req.pwn(text) RETURNS boolean
+    AS $$ BEGIN RAISE EXCEPTION 'attacker code executed'; END $$ LANGUAGE 
plpgsql;
+CREATE DOMAIN test_req.reqdom AS text CHECK (test_req.pwn(VALUE));
+CREATE TABLE test_req.reqtab(t text);
+RESET ROLE;
+-- Cache a resolution made outside any script, under the search_path the
+-- script will pin; that plan must not be reused inside the script.
+SET search_path = test_req, test_reqdep, pg_temp;
+SELECT test_reqdep.reqplpgsql() AS warmed_outside_script;
+RESET search_path;
+CREATE EXTENSION test_ext_overload_req SCHEMA test_req;
+-- Every reference resolved to the required extension's objects (in
+-- test_reqdep), not the planted ones in test_req.
+SELECT c.who, c.who_cached, n.nspname AS domain_schema
+  FROM test_req.captured c
+  JOIN pg_type t ON t.oid = c.dom
+  JOIN pg_namespace n ON n.oid = t.typnamespace;
+SELECT n.nspname AS operator_func_schema
+  FROM pg_operator o
+  JOIN pg_proc p ON p.oid = o.oprcode
+  JOIN pg_namespace n ON n.oid = p.pronamespace
+ WHERE o.oprname = '###' AND o.oprnamespace = 'test_req'::regnamespace;
+SELECT 'test_reqdep.reqtab' AS tbl, count(*) FROM test_reqdep.reqtab
+UNION ALL
+SELECT 'test_req.reqtab', count(*) FROM test_req.reqtab;
+SELECT n.nspname AS opfamily_member_schema
+  FROM pg_amop a
+  JOIN pg_opfamily f ON f.oid = a.amopfamily
+  JOIN pg_operator o ON o.oid = a.amopopr
+  JOIN pg_namespace n ON n.oid = o.oprnamespace
+ WHERE f.opfname = 'reqfam';
+-- Outside the script the cached plan is good again.
+SET search_path = test_req, test_reqdep, pg_temp;
+SELECT test_reqdep.reqplpgsql() AS after_script;
+RESET search_path;
+DROP EXTENSION test_ext_overload_req;
+DROP EXTENSION test_ext_overload_req_dep;
+DROP SCHEMA test_req CASCADE;
+DROP SCHEMA test_reqdep CASCADE;
+DO $$ BEGIN
+    EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_ext_reqowner',
+                   current_database());
+END $$;
+DROP ROLE regress_ext_attacker;
+DROP ROLE regress_ext_reqowner;
diff --git a/src/test/modules/test_extensions/test_ext_overload--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload--1.0.sql
new file mode 100644
index 00000000000..63e60a1e817
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload--1.0.sql
@@ -0,0 +1,19 @@
+/* src/test/modules/test_extensions/test_ext_overload--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload" to load this file. \quit
+
+-- f() and ### take varchar.  Resolving f('abc') during this script must
+-- reach them, not a planted f(text) sibling.
+CREATE FUNCTION @[email protected](varchar) RETURNS text
+    AS $$ SELECT 'extension'::text $$ LANGUAGE sql IMMUTABLE;
+
+CREATE FUNCTION @[email protected](varchar, varchar) RETURNS text
+    AS $$ SELECT 'extension'::text $$ LANGUAGE sql IMMUTABLE;
+
+CREATE OPERATOR @extschema@.### (leftarg = varchar, rightarg = varchar,
+                                 function = @[email protected]);
+
+CREATE TABLE @[email protected] AS
+    SELECT @[email protected]('abc') AS fn,
+           ('a' OPERATOR(@extschema@.###) 'b') AS op;
diff --git a/src/test/modules/test_extensions/test_ext_overload.control 
b/src/test/modules/test_extensions/test_ext_overload.control
new file mode 100644
index 00000000000..efaef1cb554
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload.control
@@ -0,0 +1,3 @@
+comment = 'Test protection against overload capture during extension scripts'
+default_version = '1.0'
+relocatable = false
diff --git 
a/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0--2.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0--2.0.sql
new file mode 100644
index 00000000000..f63cfcc02a6
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0--2.0.sql
@@ -0,0 +1,9 @@
+/* src/test/modules/test_extensions/test_ext_overload_nosuper--1.0--2.0.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION test_ext_overload_nosuper UPDATE" to load this 
file. \quit
+
+-- g() belongs to this extension but is owned by the non-superuser who
+-- installed 1.0, so an update run by any other role reaches it only by
+-- extension membership.
+CREATE TABLE @[email protected] AS SELECT g('abc') AS fn;
diff --git 
a/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0.sql
new file mode 100644
index 00000000000..c3e93110825
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_nosuper--1.0.sql
@@ -0,0 +1,12 @@
+/* src/test/modules/test_extensions/test_ext_overload_nosuper--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_nosuper" to load this file. \quit
+
+-- The script runs as the invoking non-superuser, so g() is owned by that
+-- role.  g('abc') must still resolve to it, not to a planted g(text).
+CREATE FUNCTION @[email protected](varchar) RETURNS text
+    AS $$ SELECT 'extension'::text $$ LANGUAGE sql IMMUTABLE;
+
+CREATE TABLE @[email protected]_nosuper AS
+    SELECT @[email protected]('abc') AS fn;
diff --git a/src/test/modules/test_extensions/test_ext_overload_nosuper.control 
b/src/test/modules/test_extensions/test_ext_overload_nosuper.control
new file mode 100644
index 00000000000..eb748089e70
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_nosuper.control
@@ -0,0 +1,4 @@
+comment = 'Test overload-capture protection for a superuser = false extension'
+default_version = '1.0'
+relocatable = false
+superuser = false
diff --git 
a/src/test/modules/test_extensions/test_ext_overload_parallel--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_parallel--1.0.sql
new file mode 100644
index 00000000000..14a46bca590
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_parallel--1.0.sql
@@ -0,0 +1,15 @@
+/* src/test/modules/test_extensions/test_ext_overload_parallel--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_parallel" to load this file. 
\quit
+
+-- wrap() is parallel safe and its body is parsed at run time, so under
+-- debug_parallel_query the worker resolves probe('x').  It must reach
+-- probe(varchar), not a planted probe(text).
+CREATE FUNCTION @[email protected](varchar) RETURNS text
+    AS $$ SELECT 'extension'::text $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION @[email protected]() RETURNS text
+    LANGUAGE plpgsql PARALLEL SAFE AS $$ BEGIN RETURN probe('x'); END $$;
+
+CREATE TABLE @[email protected] AS SELECT @[email protected]() AS who;
diff --git 
a/src/test/modules/test_extensions/test_ext_overload_parallel.control 
b/src/test/modules/test_extensions/test_ext_overload_parallel.control
new file mode 100644
index 00000000000..ba3cbae2162
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_parallel.control
@@ -0,0 +1,3 @@
+comment = 'Test overload-capture protection when resolution runs in a parallel 
worker'
+default_version = '1.0'
+relocatable = false
diff --git a/src/test/modules/test_extensions/test_ext_overload_req--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_req--1.0.sql
new file mode 100644
index 00000000000..2e7d82a0e34
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_req--1.0.sql
@@ -0,0 +1,20 @@
+/* src/test/modules/test_extensions/test_ext_overload_req--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_req" to load this file. \quit
+
+-- Every reference below is to a required extension's object.  A same-named
+-- plant in @extschema@ must not capture any of them.
+CREATE TABLE @[email protected] AS
+    SELECT reqcall(1) AS who,
+           reqplpgsql() AS who_cached,
+           pg_catalog.pg_typeof('abc'::reqdom) AS dom;
+
+INSERT INTO reqtab VALUES ('from script');
+
+CREATE OPERATOR @extschema@.### (leftarg = integer, rightarg = integer,
+                                 function = reqeq);
+
+CREATE OPERATOR FAMILY @[email protected] USING btree;
+ALTER OPERATOR FAMILY @[email protected] USING btree ADD
+    OPERATOR 3 === (integer, integer);
diff --git a/src/test/modules/test_extensions/test_ext_overload_req.control 
b/src/test/modules/test_extensions/test_ext_overload_req.control
new file mode 100644
index 00000000000..947556db238
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_req.control
@@ -0,0 +1,4 @@
+comment = 'extension whose script references a required extension''s objects'
+default_version = '1.0'
+relocatable = false
+requires = 'test_ext_overload_req_dep'
diff --git 
a/src/test/modules/test_extensions/test_ext_overload_req_dep--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_req_dep--1.0.sql
new file mode 100644
index 00000000000..be42553a86d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_req_dep--1.0.sql
@@ -0,0 +1,23 @@
+/* src/test/modules/test_extensions/test_ext_overload_req_dep--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_req_dep" to load this file. \quit
+
+-- Objects the dependent extension's script references by unqualified name.
+CREATE FUNCTION @[email protected](int) RETURNS text
+    AS $$ SELECT 'required'::text $$ LANGUAGE sql IMMUTABLE;
+
+CREATE FUNCTION @[email protected](int, int) RETURNS boolean
+    AS $$ SELECT true $$ LANGUAGE sql IMMUTABLE;
+
+CREATE DOMAIN @[email protected] AS text;
+
+CREATE TABLE @[email protected](t text);
+
+-- Parsed at run time, so a call before the dependent script caches a
+-- resolution made without trust checks.
+CREATE FUNCTION @[email protected]() RETURNS text
+    LANGUAGE plpgsql AS $$ BEGIN RETURN reqcall(1); END $$;
+
+CREATE OPERATOR @extschema@.=== (leftarg = integer, rightarg = integer,
+                                 function = @[email protected]);
diff --git a/src/test/modules/test_extensions/test_ext_overload_req_dep.control 
b/src/test/modules/test_extensions/test_ext_overload_req_dep.control
new file mode 100644
index 00000000000..f6dfb5fa874
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_req_dep.control
@@ -0,0 +1,4 @@
+comment = 'required extension providing objects for the overload-capture test'
+default_version = '1.0'
+relocatable = false
+superuser = false
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--1.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--1.0.sql
new file mode 100644
index 00000000000..13b4a8982ac
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--1.0.sql
@@ -0,0 +1,9 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- helper_only() exists only as a planted definition, so the script must
+-- fail with "function does not exist".
+CREATE TABLE @[email protected] AS
+    SELECT @[email protected]_only('abc') AS r;
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--2.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--2.0.sql
new file mode 100644
index 00000000000..f80b2d00c25
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--2.0.sql
@@ -0,0 +1,9 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--2.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- As for helper_only() in 1.0, but for an operator: the only definition of
+-- @@@ is one an unprivileged user planted, so the script must refuse it.
+CREATE TABLE @[email protected] AS
+    SELECT ('a' OPERATOR(@extschema@.@@@) 'b') AS r;
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--3.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--3.0.sql
new file mode 100644
index 00000000000..edd01344236
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--3.0.sql
@@ -0,0 +1,10 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--3.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- The only <<< (varchar, varchar) is a plant; naming it as COMMUTATOR must
+-- fail as "does not exist", not collide with it when making a shell.
+CREATE OPERATOR @extschema@.>>> (leftarg = varchar, rightarg = varchar,
+                                 function = @[email protected],
+                                 commutator = <<<);
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--4.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--4.0.sql
new file mode 100644
index 00000000000..5b7a26db851
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--4.0.sql
@@ -0,0 +1,9 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--4.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- Trusted f(varchar) and planted f(text) are visible; f(1) matches neither,
+-- so the error is about the argument types.
+CREATE TABLE @[email protected] AS
+    SELECT @[email protected](1) AS r;
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--5.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--5.0.sql
new file mode 100644
index 00000000000..1ef3115eb1b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--5.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--5.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- As in 4.0, for an operator: integer operands match neither ###.
+CREATE TABLE @[email protected] AS
+    SELECT (1 OPERATOR(@extschema@.###) 2) AS r;
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict--6.0.sql 
b/src/test/modules/test_extensions/test_ext_overload_strict--6.0.sql
new file mode 100644
index 00000000000..195e6d0e421
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict--6.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_extensions/test_ext_overload_strict--6.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_overload_strict" to load this file. \quit
+
+-- The only &&& is outside the search path; report that, not trust.
+CREATE TABLE @[email protected] AS
+    SELECT ('a' &&& 'b') AS r;
diff --git a/src/test/modules/test_extensions/test_ext_overload_strict.control 
b/src/test/modules/test_extensions/test_ext_overload_strict.control
new file mode 100644
index 00000000000..d537687c91f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_overload_strict.control
@@ -0,0 +1,3 @@
+comment = 'Test refusal to call an untrusted function during extension scripts'
+default_version = '1.0'
+relocatable = false
-- 
2.47.3

Reply via email to