Skip to content

Commit

Permalink
Add expression cost limit (#42)
Browse files Browse the repository at this point in the history
Support for expression cost limits within policy templates.
  • Loading branch information
Minmin authored Jan 15, 2021
1 parent 38245df commit 4a0d190
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 6 deletions.
1 change: 1 addition & 0 deletions policy/compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func TestCompiler(t *testing.T) {
limits.ValidatorTermLimit = 20
limits.ValidatorProductionLimit = 15
limits.RuleLimit = 4
limits.EvaluatorExprCostLimit = 100
comp := NewCompiler(reg, limits)
for _, tc := range tests {
tst := tc
Expand Down
8 changes: 7 additions & 1 deletion policy/limits/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewLimits() *Limits {
ValidatorTermLimit: 40,
ValidatorProductionLimit: 20,
RuleLimit: 10,
EvaluatorExprCostLimit: -1,
}
}

Expand Down Expand Up @@ -73,5 +74,10 @@ type Limits struct {
// Defaults to 10.
RuleLimit int

// TODO: expression size limits
// EvaluatorExprCostLimit limits the total cost of expressions which may appear within a
// template evaluator. The cost is for evaluating the expressions and is computed heuristically.
// A negative limit value is equivalent to unlimited.

// Defaults to -1.
EvaluatorExprCostLimit int
}
9 changes: 9 additions & 0 deletions policy/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ func RuleLimit(limit int) EngineOption {
}
}

// EvaluatorExprCostLimit sets the evaluator expression cost limit supported by the compilation and
// runtime components.
func EvaluatorExprCostLimit(limit int) EngineOption {
return func(e *Engine) (*Engine, error) {
e.limits.EvaluatorExprCostLimit = limit
return e, nil
}
}

// RuntimeTemplateOptions collects a set of runtime specific options to be configured on runtime
// templates.
func RuntimeTemplateOptions(rtOpts ...runtime.TemplateOption) EngineOption {
Expand Down
32 changes: 29 additions & 3 deletions policy/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package runtime

import (
"fmt"
"math"
"reflect"
"sync"

Expand Down Expand Up @@ -67,7 +68,7 @@ func NewTemplate(res model.Resolver,
"validator production limit set to %d, but %d found",
t.limits.ValidatorProductionLimit, prodCnt)
}
val, err := t.newEvaluator(mdl.Validator, t.exprOpts...)
val, err := t.newEvaluator(mdl.Validator, -1, t.exprOpts...)
if err != nil {
return nil, err
}
Expand All @@ -86,7 +87,7 @@ func NewTemplate(res model.Resolver,
"evaluator production limit set to %d, but %d found",
t.limits.EvaluatorProductionLimit, prodCnt)
}
eval, err := t.newEvaluator(mdl.Evaluator, t.exprOpts...)
eval, err := t.newEvaluator(mdl.Evaluator, t.limits.EvaluatorExprCostLimit, t.exprOpts...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -134,7 +135,7 @@ func (t *Template) Name() string {
// Validate checks the content of an instance to ensure it conforms with the validation rules
// present within the template, if any.
func (t *Template) Validate(src *model.Source, inst *model.Instance) *cel.Issues {
if t.validator == nil {
if t == nil || t.validator == nil {
return nil
}
errs := common.NewErrors(src)
Expand Down Expand Up @@ -237,6 +238,7 @@ func (t *Template) evalInternal(eval *evaluator,
}

func (t *Template) newEvaluator(mdl *model.Evaluator,
exprCostLimit int,
evalOpts ...cel.ProgramOption) (*evaluator, error) {
terms := make(map[string]cel.Program, len(mdl.Terms))
evalOpts = append(evalOpts, cel.EvalOptions(cel.OptOptimize))
Expand All @@ -250,13 +252,16 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
"range limit set to %d, but %d found",
t.limits.RangeLimit, rangeCnt)
}
var cost int64
ranges := make([]iterable, rangeCnt)
for i, r := range mdl.Ranges {
rangeType := r.Expr.ResultType()
rangePrg, err := env.Program(r.Expr)
if err != nil {
return nil, err
}
_, max := cel.EstimateCost(rangePrg)
cost = addAndCap(cost, max)
switch rangeType.TypeKind.(type) {
case *exprpb.Type_MapType_:
mr := &mapRange{
Expand All @@ -280,6 +285,8 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
return nil, err
}
terms[t.Name] = term
_, max := cel.EstimateCost(term)
cost = addAndCap(cost, max)
}

prods := make([]*prod, len(mdl.Productions))
Expand All @@ -290,6 +297,8 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
if err != nil {
return nil, err
}
_, max := cel.EstimateCost(match)
cost = addAndCap(cost, max)
decCnt := len(p.Decisions)
if decCnt > t.limits.EvaluatorDecisionLimit {
return nil, fmt.Errorf(
Expand All @@ -302,6 +311,8 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
if err != nil {
return nil, err
}
_, max := cel.EstimateCost(dec)
cost = addAndCap(cost, max)
slot, found := decSlotMap[d.Name]
if !found {
slot = nextSlot
Expand All @@ -324,6 +335,12 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
decisions: decs,
}
}
// Cost is greater than the limit.
if exprCostLimit >= 0 && cost > int64(exprCostLimit) {
return nil, fmt.Errorf(
"evaluator expression cost limit set to %d, but %d found",
exprCostLimit, cost)
}
eval := &evaluator{
mdl: mdl,
env: env,
Expand All @@ -335,6 +352,15 @@ func (t *Template) newEvaluator(mdl *model.Evaluator,
return eval, nil
}

// addAndCap returns the max int64 if the cost overflows after the addition.
func addAndCap(cost, addend int64) int64 {
result := cost + addend
if result < 0 {
return math.MaxInt64
}
return result
}

func (t *Template) newEnv(name string) (*cel.Env, error) {
env := stdEnv
if name != "" {
Expand Down
3 changes: 2 additions & 1 deletion test/testdata/limit/instance.compile.err
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ERROR: ../../test/testdata/limit/instance.yaml:24:5: rule limit set to 4, but 5 found
ERROR: ../../test/testdata/limit/instance.yaml:-1:0: evaluator expression cost limit set to 100, but 9223372036854775807 found
ERROR: ../../test/testdata/limit/instance.yaml:25:5: rule limit set to 4, but 5 found
| - greeting: "Good night"
| ....^
1 change: 1 addition & 0 deletions test/testdata/limit/instance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ metadata:
name: limit_instance_example
rules:
- greeting: "Hello"
encounters: ["doga", "dogb", "catc"]
- greeting: "Good morning"
- greeting: "Good afternoon"
- greeting: "Good evening"
Expand Down
9 changes: 8 additions & 1 deletion test/testdata/limit/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ schema:
properties:
greeting:
type: string

encounters:
type: array
items:
type: string
evaluator:
terms:
hi: rule.greeting
productions:
- match: hi != ''
decision: policy.acme.welcome
output: hi
- match: >
rule.encounters.exists(encounter, encounter == "vikings")
decision: policy.acme.welcome
output: hi
1 change: 1 addition & 0 deletions test/testdata/required_labels/instance.compile.err
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: ../../test/testdata/required_labels/instance.yaml:-1:0: evaluator expression cost limit set to 100, but 9223372036854775807 found
Empty file.
1 change: 1 addition & 0 deletions test/testdata/sensitive_data/instance.compile.err
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: ../../test/testdata/sensitive_data/instance.yaml:-1:0: evaluator expression cost limit set to 100, but 9223372036854775807 found
Empty file.

0 comments on commit 4a0d190

Please sign in to comment.