Require superuser to install a non-built-in selectivity estimator.

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
pull/271/head
Tom Lane 6 days ago
parent 60e7ae41a6
commit 841d42cc4e
  1. 57
      src/backend/commands/operatorcmds.c
  2. 8
      src/backend/tsearch/ts_selfuncs.c
  3. 48
      src/backend/utils/adt/network_selfuncs.c

@ -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;
}

@ -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

@ -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

Loading…
Cancel
Save