Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 215b43c

Browse files
committed
Improve RLS planning by marking individual quals with security levels.
In an RLS query, we must ensure that security filter quals are evaluated before ordinary query quals, in case the latter contain "leaky" functions that could expose the contents of sensitive rows. The original implementation of RLS planning ensured this by pushing the scan of a secured table into a sub-query that it marked as a security-barrier view. Unfortunately this results in very inefficient plans in many cases, because the sub-query cannot be flattened and gets planned independently of the rest of the query. To fix, drop the use of sub-queries to enforce RLS qual order, and instead mark each qual (RestrictInfo) with a security_level field establishing its priority for evaluation. Quals must be evaluated in security_level order, except that "leakproof" quals can be allowed to go ahead of quals of lower security_level, if it's helpful to do so. This has to be enforced within the ordering of any one list of quals to be evaluated at a table scan node, and we also have to ensure that quals are not chosen for early evaluation (i.e., use as an index qual or TID scan qual) if they're not allowed to go ahead of other quals at the scan node. This is sufficient to fix the problem for RLS quals, since we only support RLS policies on simple tables and thus RLS quals will always exist at the table scan level only. Eventually these qual ordering rules should be enforced for join quals as well, which would permit improving planning for explicit security-barrier views; but that's a task for another patch. Note that FDWs would need to be aware of these rules --- and not, for example, send an insecure qual for remote execution --- but since we do not yet allow RLS policies on foreign tables, the case doesn't arise. This will need to be addressed before we can allow such policies. Patch by me, reviewed by Stephen Frost and Dean Rasheed. Discussion: https://postgr.es/m/8185.1477432701@sss.pgh.pa.us
1 parent aa17c06 commit 215b43c

29 files changed

+1137
-1558
lines changed

src/backend/nodes/copyfuncs.c

+2
Original file line numberDiff line numberDiff line change
@@ -2027,6 +2027,8 @@ _copyRestrictInfo(const RestrictInfo *from)
20272027
COPY_SCALAR_FIELD(outerjoin_delayed);
20282028
COPY_SCALAR_FIELD(can_join);
20292029
COPY_SCALAR_FIELD(pseudoconstant);
2030+
COPY_SCALAR_FIELD(leakproof);
2031+
COPY_SCALAR_FIELD(security_level);
20302032
COPY_BITMAPSET_FIELD(clause_relids);
20312033
COPY_BITMAPSET_FIELD(required_relids);
20322034
COPY_BITMAPSET_FIELD(outer_relids);

src/backend/nodes/equalfuncs.c

+1
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,7 @@ _equalRestrictInfo(const RestrictInfo *a, const RestrictInfo *b)
805805
COMPARE_NODE_FIELD(clause);
806806
COMPARE_SCALAR_FIELD(is_pushed_down);
807807
COMPARE_SCALAR_FIELD(outerjoin_delayed);
808+
COMPARE_SCALAR_FIELD(security_level);
808809
COMPARE_BITMAPSET_FIELD(required_relids);
809810
COMPARE_BITMAPSET_FIELD(outer_relids);
810811
COMPARE_BITMAPSET_FIELD(nullable_relids);

src/backend/nodes/outfuncs.c

+6
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
20592059
WRITE_FLOAT_FIELD(total_table_pages, "%.0f");
20602060
WRITE_FLOAT_FIELD(tuple_fraction, "%.4f");
20612061
WRITE_FLOAT_FIELD(limit_tuples, "%.0f");
2062+
WRITE_UINT_FIELD(qual_security_level);
20622063
WRITE_BOOL_FIELD(hasInheritedTarget);
20632064
WRITE_BOOL_FIELD(hasJoinRTEs);
20642065
WRITE_BOOL_FIELD(hasLateralRTEs);
@@ -2112,6 +2113,7 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
21122113
WRITE_BOOL_FIELD(useridiscurrent);
21132114
/* we don't try to print fdwroutine or fdw_private */
21142115
WRITE_NODE_FIELD(baserestrictinfo);
2116+
WRITE_UINT_FIELD(baserestrict_min_security);
21152117
WRITE_NODE_FIELD(joininfo);
21162118
WRITE_BOOL_FIELD(has_eclass_joins);
21172119
}
@@ -2195,6 +2197,8 @@ _outEquivalenceClass(StringInfo str, const EquivalenceClass *node)
21952197
WRITE_BOOL_FIELD(ec_below_outer_join);
21962198
WRITE_BOOL_FIELD(ec_broken);
21972199
WRITE_UINT_FIELD(ec_sortref);
2200+
WRITE_UINT_FIELD(ec_min_security);
2201+
WRITE_UINT_FIELD(ec_max_security);
21982202
}
21992203

22002204
static void
@@ -2261,6 +2265,8 @@ _outRestrictInfo(StringInfo str, const RestrictInfo *node)
22612265
WRITE_BOOL_FIELD(outerjoin_delayed);
22622266
WRITE_BOOL_FIELD(can_join);
22632267
WRITE_BOOL_FIELD(pseudoconstant);
2268+
WRITE_BOOL_FIELD(leakproof);
2269+
WRITE_UINT_FIELD(security_level);
22642270
WRITE_BITMAPSET_FIELD(clause_relids);
22652271
WRITE_BITMAPSET_FIELD(required_relids);
22662272
WRITE_BITMAPSET_FIELD(outer_relids);

src/backend/optimizer/README

+102
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,108 @@ lateral reference. (Perhaps now that that stuff works, we could relax the
877877
pullup restriction?)
878878

879879

880+
Security-level constraints on qual clauses
881+
------------------------------------------
882+
883+
To support row-level security and security-barrier views efficiently,
884+
we mark qual clauses (RestrictInfo nodes) with a "security_level" field.
885+
The basic concept is that a qual with a lower security_level must be
886+
evaluated before one with a higher security_level. This ensures that
887+
"leaky" quals that might expose sensitive data are not evaluated until
888+
after the security barrier quals that are supposed to filter out
889+
security-sensitive rows. However, many qual conditions are "leakproof",
890+
that is we trust the functions they use to not expose data. To avoid
891+
unnecessarily inefficient plans, a leakproof qual is not delayed by
892+
security-level considerations, even if it has a higher syntactic
893+
security_level than another qual.
894+
895+
In a query that contains no use of RLS or security-barrier views, all
896+
quals will have security_level zero, so that none of these restrictions
897+
kick in; we don't even need to check leakproofness of qual conditions.
898+
899+
If there are security-barrier quals, they get security_level zero (and
900+
possibly higher, if there are multiple layers of barriers). Regular quals
901+
coming from the query text get a security_level one more than the highest
902+
level used for barrier quals.
903+
904+
When new qual clauses are generated by EquivalenceClass processing,
905+
they must be assigned a security_level. This is trickier than it seems.
906+
One's first instinct is that it would be safe to use the largest level
907+
found among the source quals for the EquivalenceClass, but that isn't
908+
safe at all, because it allows unwanted delays of security-barrier quals.
909+
Consider a barrier qual "t.x = t.y" plus a query qual "t.x = constant",
910+
and suppose there is another query qual "leaky_function(t.z)" that
911+
we mustn't evaluate before the barrier qual has been checked.
912+
We will have an EC {t.x, t.y, constant} which will lead us to replace
913+
the EC quals with "t.x = constant AND t.y = constant". (We do not want
914+
to give up that behavior, either, since the latter condition could allow
915+
use of an index on t.y, which we would never discover from the original
916+
quals.) If these generated quals are assigned the same security_level as
917+
the query quals, then it's possible for the leaky_function qual to be
918+
evaluated first, allowing leaky_function to see data from rows that
919+
possibly don't pass the barrier condition.
920+
921+
Instead, our handling of security levels with ECs works like this:
922+
* Quals are not accepted as source clauses for ECs in the first place
923+
unless they are leakproof or have security_level zero.
924+
* EC-derived quals are assigned the minimum (not maximum) security_level
925+
found among the EC's source clauses.
926+
* If the maximum security_level found among the EC's source clauses is
927+
above zero, then the equality operators selected for derived quals must
928+
be leakproof. When no such operator can be found, the EC is treated as
929+
"broken" and we fall back to emitting its source clauses without any
930+
additional derived quals.
931+
932+
These rules together ensure that an untrusted qual clause (one with
933+
security_level above zero) cannot cause an EC to generate a leaky derived
934+
clause. This makes it safe to use the minimum not maximum security_level
935+
for derived clauses. The rules could result in poor plans due to not
936+
being able to generate derived clauses at all, but the risk of that is
937+
small in practice because most btree equality operators are leakproof.
938+
Also, by making exceptions for level-zero quals, we ensure that there is
939+
no plan degradation when no barrier quals are present.
940+
941+
Once we have security levels assigned to all clauses, enforcement
942+
of barrier-qual ordering restrictions boils down to two rules:
943+
944+
* Table scan plan nodes must not select quals for early execution
945+
(for example, use them as index qualifiers in an indexscan) unless
946+
they are leakproof or have security_level no higher than any other
947+
qual that is due to be executed at the same plan node. (Use the
948+
utility function restriction_is_securely_promotable() to check
949+
whether it's okay to select a qual for early execution.)
950+
951+
* Normal execution of a list of quals must execute them in an order
952+
that satisfies the same security rule, ie higher security_levels must
953+
be evaluated later unless leakproof. (This is handled in a single place
954+
by order_qual_clauses() in createplan.c.)
955+
956+
order_qual_clauses() uses a heuristic to decide exactly what to do with
957+
leakproof clauses. Normally it sorts clauses by security_level then cost,
958+
being careful that the sort is stable so that we don't reorder clauses
959+
without a clear reason. But this could result in a very expensive qual
960+
being done before a cheaper one that is of higher security_level.
961+
If the cheaper qual is leaky we have no choice, but if it is leakproof
962+
we could put it first. We choose to sort leakproof quals as if they
963+
have security_level zero, but only when their cost is less than 10X
964+
cpu_operator_cost; that restriction alleviates the opposite problem of
965+
doing expensive quals first just because they're leakproof.
966+
967+
Additional rules will be needed to support safe handling of join quals
968+
when there is a mix of security levels among join quals; for example, it
969+
will be necessary to prevent leaky higher-security-level quals from being
970+
evaluated at a lower join level than other quals of lower security level.
971+
Currently there is no need to consider that since security-prioritized
972+
quals can only be single-table restriction quals coming from RLS policies
973+
or security-barrier views, and security-barrier view subqueries are never
974+
flattened into the parent query. Hence enforcement of security-prioritized
975+
quals only happens at the table scan level. With extra rules for safe
976+
handling of security levels among join quals, it should be possible to let
977+
security-barrier views be flattened into the parent query, allowing more
978+
flexibility of planning while still preserving required ordering of qual
979+
evaluation. But that will come later.
980+
981+
880982
Post scan/join planning
881983
-----------------------
882984

src/backend/optimizer/path/allpaths.c

+104-62
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,11 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
896896
RangeTblEntry *childRTE;
897897
RelOptInfo *childrel;
898898
List *childquals;
899-
Node *childqual;
899+
Index cq_min_security;
900+
bool have_const_false_cq;
900901
ListCell *parentvars;
901902
ListCell *childvars;
903+
ListCell *lc;
902904

903905
/* append_rel_list contains all append rels; ignore others */
904906
if (appinfo->parent_relid != parentRTindex)
@@ -921,34 +923,113 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
921923
* constraint exclusion; so do that first and then check to see if we
922924
* can disregard this child.
923925
*
924-
* As of 8.4, the child rel's targetlist might contain non-Var
925-
* expressions, which means that substitution into the quals could
926-
* produce opportunities for const-simplification, and perhaps even
927-
* pseudoconstant quals. To deal with this, we strip the RestrictInfo
928-
* nodes, do the substitution, do const-simplification, and then
929-
* reconstitute the RestrictInfo layer.
926+
* The child rel's targetlist might contain non-Var expressions, which
927+
* means that substitution into the quals could produce opportunities
928+
* for const-simplification, and perhaps even pseudoconstant quals.
929+
* Therefore, transform each RestrictInfo separately to see if it
930+
* reduces to a constant or pseudoconstant. (We must process them
931+
* separately to keep track of the security level of each qual.)
932+
*/
933+
childquals = NIL;
934+
cq_min_security = UINT_MAX;
935+
have_const_false_cq = false;
936+
foreach(lc, rel->baserestrictinfo)
937+
{
938+
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
939+
Node *childqual;
940+
bool pseudoconstant;
941+
942+
Assert(IsA(rinfo, RestrictInfo));
943+
childqual = adjust_appendrel_attrs(root,
944+
(Node *) rinfo->clause,
945+
appinfo);
946+
childqual = eval_const_expressions(root, childqual);
947+
/* check for flat-out constant */
948+
if (childqual && IsA(childqual, Const))
949+
{
950+
if (((Const *) childqual)->constisnull ||
951+
!DatumGetBool(((Const *) childqual)->constvalue))
952+
{
953+
/* Restriction reduces to constant FALSE or NULL */
954+
have_const_false_cq = true;
955+
break;
956+
}
957+
/* Restriction reduces to constant TRUE, so drop it */
958+
continue;
959+
}
960+
/* check for pseudoconstant (no Vars or volatile functions) */
961+
pseudoconstant =
962+
!contain_vars_of_level(childqual, 0) &&
963+
!contain_volatile_functions(childqual);
964+
if (pseudoconstant)
965+
{
966+
/* tell createplan.c to check for gating quals */
967+
root->hasPseudoConstantQuals = true;
968+
}
969+
/* reconstitute RestrictInfo with appropriate properties */
970+
childquals = lappend(childquals,
971+
make_restrictinfo((Expr *) childqual,
972+
rinfo->is_pushed_down,
973+
rinfo->outerjoin_delayed,
974+
pseudoconstant,
975+
rinfo->security_level,
976+
NULL, NULL, NULL));
977+
/* track minimum security level among child quals */
978+
cq_min_security = Min(cq_min_security, rinfo->security_level);
979+
}
980+
981+
/*
982+
* In addition to the quals inherited from the parent, we might have
983+
* securityQuals associated with this particular child node.
984+
* (Currently this can only happen in appendrels originating from
985+
* UNION ALL; inheritance child tables don't have their own
986+
* securityQuals, see expand_inherited_rtentry().) Pull any such
987+
* securityQuals up into the baserestrictinfo for the child. This is
988+
* similar to process_security_barrier_quals() for the parent rel,
989+
* except that we can't make any general deductions from such quals,
990+
* since they don't hold for the whole appendrel.
991+
*/
992+
if (childRTE->securityQuals)
993+
{
994+
Index security_level = 0;
995+
996+
foreach(lc, childRTE->securityQuals)
997+
{
998+
List *qualset = (List *) lfirst(lc);
999+
ListCell *lc2;
1000+
1001+
foreach(lc2, qualset)
1002+
{
1003+
Expr *qual = (Expr *) lfirst(lc2);
1004+
1005+
/* not likely that we'd see constants here, so no check */
1006+
childquals = lappend(childquals,
1007+
make_restrictinfo(qual,
1008+
true, false, false,
1009+
security_level,
1010+
NULL, NULL, NULL));
1011+
cq_min_security = Min(cq_min_security, security_level);
1012+
}
1013+
security_level++;
1014+
}
1015+
Assert(security_level <= root->qual_security_level);
1016+
}
1017+
1018+
/*
1019+
* OK, we've got all the baserestrictinfo quals for this child.
9301020
*/
931-
childquals = get_all_actual_clauses(rel->baserestrictinfo);
932-
childquals = (List *) adjust_appendrel_attrs(root,
933-
(Node *) childquals,
934-
appinfo);
935-
childqual = eval_const_expressions(root, (Node *)
936-
make_ands_explicit(childquals));
937-
if (childqual && IsA(childqual, Const) &&
938-
(((Const *) childqual)->constisnull ||
939-
!DatumGetBool(((Const *) childqual)->constvalue)))
1021+
childrel->baserestrictinfo = childquals;
1022+
childrel->baserestrict_min_security = cq_min_security;
1023+
1024+
if (have_const_false_cq)
9401025
{
9411026
/*
942-
* Restriction reduces to constant FALSE or constant NULL after
1027+
* Some restriction clause reduced to constant FALSE or NULL after
9431028
* substitution, so this child need not be scanned.
9441029
*/
9451030
set_dummy_rel_pathlist(childrel);
9461031
continue;
9471032
}
948-
childquals = make_ands_implicit((Expr *) childqual);
949-
childquals = make_restrictinfos_from_actual_clauses(root,
950-
childquals);
951-
childrel->baserestrictinfo = childquals;
9521033

9531034
if (relation_excluded_by_constraints(root, childrel, childRTE))
9541035
{
@@ -1712,6 +1793,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
17121793
}
17131794
}
17141795
rel->baserestrictinfo = upperrestrictlist;
1796+
/* We don't bother recomputing baserestrict_min_security */
17151797
}
17161798

17171799
pfree(safetyInfo.unsafeColumns);
@@ -2640,46 +2722,6 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
26402722
recurse_push_qual(subquery->setOperations, subquery,
26412723
rte, rti, qual);
26422724
}
2643-
else if (IsA(qual, CurrentOfExpr))
2644-
{
2645-
/*
2646-
* This is possible when a WHERE CURRENT OF expression is applied to a
2647-
* table with row-level security. In that case, the subquery should
2648-
* contain precisely one rtable entry for the table, and we can safely
2649-
* push the expression down into the subquery. This will cause a TID
2650-
* scan subquery plan to be generated allowing the target relation to
2651-
* be updated.
2652-
*
2653-
* Someday we might also be able to use a WHERE CURRENT OF expression
2654-
* on a view, but currently the rewriter prevents that, so we should
2655-
* never see any other case here, but generate sane error messages in
2656-
* case it does somehow happen.
2657-
*/
2658-
if (subquery->rtable == NIL)
2659-
ereport(ERROR,
2660-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2661-
errmsg("WHERE CURRENT OF is not supported on a view with no underlying relation")));
2662-
2663-
if (list_length(subquery->rtable) > 1)
2664-
ereport(ERROR,
2665-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2666-
errmsg("WHERE CURRENT OF is not supported on a view with more than one underlying relation")));
2667-
2668-
if (subquery->hasAggs || subquery->groupClause || subquery->groupingSets || subquery->havingQual)
2669-
ereport(ERROR,
2670-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2671-
errmsg("WHERE CURRENT OF is not supported on a view with grouping or aggregation")));
2672-
2673-
/*
2674-
* Adjust the CURRENT OF expression to refer to the underlying table
2675-
* in the subquery, and attach it to the subquery's WHERE clause.
2676-
*/
2677-
qual = copyObject(qual);
2678-
((CurrentOfExpr *) qual)->cvarno = 1;
2679-
2680-
subquery->jointree->quals =
2681-
make_and_qual(subquery->jointree->quals, qual);
2682-
}
26832725
else
26842726
{
26852727
/*
@@ -2708,7 +2750,7 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
27082750
make_and_qual(subquery->jointree->quals, qual);
27092751

27102752
/*
2711-
* We need not change the subquery's hasAggs or hasSublinks flags,
2753+
* We need not change the subquery's hasAggs or hasSubLinks flags,
27122754
* since we can't be pushing down any aggregates that weren't there
27132755
* before, and we don't push down subselects at all.
27142756
*/

0 commit comments

Comments
 (0)