]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Require superuser to install a non-built-in selectivity estimator.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 9 Feb 2026 15:07:31 +0000 (10:07 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 9 Feb 2026 15:07:31 +0000 (10:07 -0500)
Selectivity estimators come in two flavors: those that make specific
assumptions about the data types they are working with, and those
that don't.  Most of the built-in estimators are of the latter kind
and are meant to be safely attachable to any operator.  If the
operator does not behave as the estimator expects, you might get a
poor estimate, but it won't crash.

However, estimators that do make datatype assumptions can malfunction
if they are attached to the wrong operator, since then the data they
get from pg_statistic may not be of the type they expect.  This can
rise to the level of a security problem, even permitting arbitrary
code execution by a user who has the ability to create SQL objects.

To close this hole, establish a rule that built-in estimators are
required to protect themselves against being called on the wrong type
of data.  It does not seem practical however to expect estimators in
extensions to reach a similar level of security, at least not in the
near term.  Therefore, also establish a rule that superuser privilege
is required to attach a non-built-in estimator to an operator.
We expect that this restriction will have little negative impact on
extensions, since estimators generally have to be written in C and
thus superuser privilege is required to create them in the first
place.

This commit changes the privilege checks in CREATE/ALTER OPERATOR
to enforce the rule about superuser privilege, and fixes a couple
of built-in estimators that were making datatype assumptions without
sufficiently checking that they're valid.

Reported-by: Daniel Firer as part of zeroday.cloud
Author: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Noah Misch <noah@leadboat.com>
Security: CVE-2026-2004
Backpatch-through: 14

src/backend/commands/operatorcmds.c
src/backend/tsearch/ts_selfuncs.c
src/backend/utils/adt/network_selfuncs.c

index 9f7e0ed17ceafbdd63134f4b67493018fdbb5ded..3e7b09b349421181fc54eedc5a8899e0c99332dc 100644 (file)
@@ -276,7 +276,6 @@ ValidateRestrictionEstimator(List *restrictionName)
 {
        Oid                     typeId[4];
        Oid                     restrictionOid;
-       AclResult       aclresult;
 
        typeId[0] = INTERNALOID;        /* PlannerInfo */
        typeId[1] = OIDOID;                     /* operator OID */
@@ -292,11 +291,33 @@ ValidateRestrictionEstimator(List *restrictionName)
                                 errmsg("restriction estimator function %s must return type %s",
                                                NameListToString(restrictionName), "float8")));
 
-       /* Require EXECUTE rights for the estimator */
-       aclresult = object_aclcheck(ProcedureRelationId, restrictionOid, GetUserId(), ACL_EXECUTE);
-       if (aclresult != ACLCHECK_OK)
-               aclcheck_error(aclresult, OBJECT_FUNCTION,
-                                          NameListToString(restrictionName));
+       /*
+        * If the estimator is not a built-in function, require superuser
+        * privilege to install it.  This protects against using something that is
+        * not a restriction estimator or has hard-wired assumptions about what
+        * data types it is working with.  (Built-in estimators are required to
+        * defend themselves adequately against unexpected data type choices, but
+        * it seems impractical to expect that of extensions' estimators.)
+        *
+        * If it is built-in, only require EXECUTE rights.
+        */
+       if (restrictionOid >= FirstGenbkiObjectId)
+       {
+               if (!superuser())
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                                        errmsg("must be superuser to specify a non-built-in restriction estimator function")));
+       }
+       else
+       {
+               AclResult       aclresult;
+
+               aclresult = object_aclcheck(ProcedureRelationId, restrictionOid,
+                                                                       GetUserId(), ACL_EXECUTE);
+               if (aclresult != ACLCHECK_OK)
+                       aclcheck_error(aclresult, OBJECT_FUNCTION,
+                                                  NameListToString(restrictionName));
+       }
 
        return restrictionOid;
 }
@@ -312,7 +333,6 @@ ValidateJoinEstimator(List *joinName)
        Oid                     typeId[5];
        Oid                     joinOid;
        Oid                     joinOid2;
-       AclResult       aclresult;
 
        typeId[0] = INTERNALOID;        /* PlannerInfo */
        typeId[1] = OIDOID;                     /* operator OID */
@@ -350,11 +370,24 @@ ValidateJoinEstimator(List *joinName)
                                 errmsg("join estimator function %s must return type %s",
                                                NameListToString(joinName), "float8")));
 
-       /* Require EXECUTE rights for the estimator */
-       aclresult = object_aclcheck(ProcedureRelationId, joinOid, GetUserId(), ACL_EXECUTE);
-       if (aclresult != ACLCHECK_OK)
-               aclcheck_error(aclresult, OBJECT_FUNCTION,
-                                          NameListToString(joinName));
+       /* privilege checks are the same as in ValidateRestrictionEstimator */
+       if (joinOid >= FirstGenbkiObjectId)
+       {
+               if (!superuser())
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                                        errmsg("must be superuser to specify a non-built-in join estimator function")));
+       }
+       else
+       {
+               AclResult       aclresult;
+
+               aclresult = object_aclcheck(ProcedureRelationId, joinOid,
+                                                                       GetUserId(), ACL_EXECUTE);
+               if (aclresult != ACLCHECK_OK)
+                       aclcheck_error(aclresult, OBJECT_FUNCTION,
+                                                  NameListToString(joinName));
+       }
 
        return joinOid;
 }
index 5afa6e4bad8db2350a3a55e419a13454d07bd66e..64b60bb9513ecda59bc6c5ba365414b1bdb15f0a 100644 (file)
@@ -108,12 +108,14 @@ tsmatchsel(PG_FUNCTION_ARGS)
         * OK, there's a Var and a Const we're dealing with here.  We need the
         * Const to be a TSQuery, else we can't do anything useful.  We have to
         * check this because the Var might be the TSQuery not the TSVector.
+        *
+        * Also check that the Var really is a TSVector, in case this estimator is
+        * mistakenly attached to some other operator.
         */
-       if (((Const *) other)->consttype == TSQUERYOID)
+       if (((Const *) other)->consttype == TSQUERYOID &&
+               vardata.vartype == TSVECTOROID)
        {
                /* tsvector @@ tsquery or the other way around */
-               Assert(vardata.vartype == TSVECTOROID);
-
                selec = tsquerysel(&vardata, ((Const *) other)->constvalue);
        }
        else
index 902f9c25db00a0441289f16e40673f03243d156e..2a8d2ded9078f77f91da8640065cfec206ac15fc 100644 (file)
@@ -43,9 +43,9 @@
 /* Maximum number of items to consider in join selectivity calculations */
 #define MAX_CONSIDERED_ELEMS 1024
 
-static Selectivity networkjoinsel_inner(Oid operator,
+static Selectivity networkjoinsel_inner(Oid operator, int opr_codenum,
                                                                                VariableStatData *vardata1, VariableStatData *vardata2);
-static Selectivity networkjoinsel_semi(Oid operator,
+static Selectivity networkjoinsel_semi(Oid operator, int opr_codenum,
                                                                           VariableStatData *vardata1, VariableStatData *vardata2);
 static Selectivity mcv_population(float4 *mcv_numbers, int mcv_nvalues);
 static Selectivity inet_hist_value_sel(const Datum *values, int nvalues,
@@ -82,6 +82,7 @@ networksel(PG_FUNCTION_ARGS)
        Oid                     operator = PG_GETARG_OID(1);
        List       *args = (List *) PG_GETARG_POINTER(2);
        int                     varRelid = PG_GETARG_INT32(3);
+       int                     opr_codenum;
        VariableStatData vardata;
        Node       *other;
        bool            varonleft;
@@ -95,6 +96,14 @@ networksel(PG_FUNCTION_ARGS)
                                nullfrac;
        FmgrInfo        proc;
 
+       /*
+        * Before all else, verify that the operator is one of the ones supported
+        * by this function, which in turn proves that the input datatypes are
+        * what we expect.  Otherwise, attaching this selectivity function to some
+        * unexpected operator could cause trouble.
+        */
+       opr_codenum = inet_opr_codenum(operator);
+
        /*
         * If expression is not (variable op something) or (something op
         * variable), then punt and return a default estimate.
@@ -150,13 +159,12 @@ networksel(PG_FUNCTION_ARGS)
                                                 STATISTIC_KIND_HISTOGRAM, InvalidOid,
                                                 ATTSTATSSLOT_VALUES))
        {
-               int                     opr_codenum = inet_opr_codenum(operator);
+               int                     h_codenum;
 
                /* Commute if needed, so we can consider histogram to be on the left */
-               if (!varonleft)
-                       opr_codenum = -opr_codenum;
+               h_codenum = varonleft ? opr_codenum : -opr_codenum;
                non_mcv_selec = inet_hist_value_sel(hslot.values, hslot.nvalues,
-                                                                                       constvalue, opr_codenum);
+                                                                                       constvalue, h_codenum);
 
                free_attstatsslot(&hslot);
        }
@@ -203,10 +211,19 @@ networkjoinsel(PG_FUNCTION_ARGS)
 #endif
        SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) PG_GETARG_POINTER(4);
        double          selec;
+       int                     opr_codenum;
        VariableStatData vardata1;
        VariableStatData vardata2;
        bool            join_is_reversed;
 
+       /*
+        * Before all else, verify that the operator is one of the ones supported
+        * by this function, which in turn proves that the input datatypes are
+        * what we expect.  Otherwise, attaching this selectivity function to some
+        * unexpected operator could cause trouble.
+        */
+       opr_codenum = inet_opr_codenum(operator);
+
        get_join_variables(root, args, sjinfo,
                                           &vardata1, &vardata2, &join_is_reversed);
 
@@ -220,15 +237,18 @@ networkjoinsel(PG_FUNCTION_ARGS)
                         * Selectivity for left/full join is not exactly the same as inner
                         * join, but we neglect the difference, as eqjoinsel does.
                         */
-                       selec = networkjoinsel_inner(operator, &vardata1, &vardata2);
+                       selec = networkjoinsel_inner(operator, opr_codenum,
+                                                                                &vardata1, &vardata2);
                        break;
                case JOIN_SEMI:
                case JOIN_ANTI:
                        /* Here, it's important that we pass the outer var on the left. */
                        if (!join_is_reversed)
-                               selec = networkjoinsel_semi(operator, &vardata1, &vardata2);
+                               selec = networkjoinsel_semi(operator, opr_codenum,
+                                                                                       &vardata1, &vardata2);
                        else
                                selec = networkjoinsel_semi(get_commutator(operator),
+                                                                                       -opr_codenum,
                                                                                        &vardata2, &vardata1);
                        break;
                default:
@@ -260,7 +280,7 @@ networkjoinsel(PG_FUNCTION_ARGS)
  * Also, MCV vs histogram selectivity is not neglected as in eqjoinsel_inner().
  */
 static Selectivity
-networkjoinsel_inner(Oid operator,
+networkjoinsel_inner(Oid operator, int opr_codenum,
                                         VariableStatData *vardata1, VariableStatData *vardata2)
 {
        Form_pg_statistic stats;
@@ -273,7 +293,6 @@ networkjoinsel_inner(Oid operator,
                                mcv2_exists = false,
                                hist1_exists = false,
                                hist2_exists = false;
-       int                     opr_codenum;
        int                     mcv1_length = 0,
                                mcv2_length = 0;
        AttStatsSlot mcv1_slot;
@@ -325,8 +344,6 @@ networkjoinsel_inner(Oid operator,
                memset(&hist2_slot, 0, sizeof(hist2_slot));
        }
 
-       opr_codenum = inet_opr_codenum(operator);
-
        /*
         * Calculate selectivity for MCV vs MCV matches.
         */
@@ -387,7 +404,7 @@ networkjoinsel_inner(Oid operator,
  * histogram selectivity for semi/anti join cases.
  */
 static Selectivity
-networkjoinsel_semi(Oid operator,
+networkjoinsel_semi(Oid operator, int opr_codenum,
                                        VariableStatData *vardata1, VariableStatData *vardata2)
 {
        Form_pg_statistic stats;
@@ -401,7 +418,6 @@ networkjoinsel_semi(Oid operator,
                                mcv2_exists = false,
                                hist1_exists = false,
                                hist2_exists = false;
-       int                     opr_codenum;
        FmgrInfo        proc;
        int                     i,
                                mcv1_length = 0,
@@ -455,7 +471,6 @@ networkjoinsel_semi(Oid operator,
                memset(&hist2_slot, 0, sizeof(hist2_slot));
        }
 
-       opr_codenum = inet_opr_codenum(operator);
        fmgr_info(get_opcode(operator), &proc);
 
        /* Estimate number of input rows represented by RHS histogram. */
@@ -827,6 +842,9 @@ inet_semi_join_sel(Datum lhs_value,
 /*
  * Assign useful code numbers for the subnet inclusion/overlap operators
  *
+ * This will throw an error if the operator is not one of the ones we
+ * support in networksel() and networkjoinsel().
+ *
  * Only inet_masklen_inclusion_cmp() and inet_hist_match_divider() depend
  * on the exact codes assigned here; but many other places in this file
  * know that they can negate a code to obtain the code for the commutator