Skip to content

CostLimit is silently ignored and ActualCost() reports 0 when OptTrackState or OptExhaustiveEval is enabled #1425

Description

@miparnisari

Describe the bug
Since v0.25.0, enabling cel.OptTrackState (or cel.OptExhaustiveEval) together with cel.CostLimit / cel.CostTracking silently disables runtime cost tracking:

  • the cost limit is never enforced. Evaluation completes normally instead of returning EvalCancelledError{Cause: CostLimitExceeded};
  • EvalDetails.ActualCost() returns a non-nil pointer to 0 instead of the real cost.

There is no error, no warning, and no API-level hint that the limit has been disabled. Code that relies on CostLimit as a DoS guard keeps running with the guard removed.

To Reproduce
Check which components this affects:

  • parser
  • checker
  • interpreter

Sample expression and input that reproduces the issue:

a + b > 47

Test setup:

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
)

func main() {
	env, err := cel.NewEnv(cel.Variable("a", cel.IntType), cel.Variable("b", cel.IntType))
	if err != nil {
		panic(err)
	}
	ast, iss := env.Compile("a + b > 47")
	if iss.Err() != nil {
		panic(iss.Err())
	}

	cases := []struct {
		name string
		opts []cel.ProgramOption
	}{
		{"CostLimit(1)", []cel.ProgramOption{cel.CostLimit(1)}},
		{"CostLimit(1)+OptTrackState", []cel.ProgramOption{cel.EvalOptions(cel.OptTrackState), cel.CostLimit(1)}},
		{"CostLimit(1)+OptExhaustiveEval", []cel.ProgramOption{cel.EvalOptions(cel.OptExhaustiveEval), cel.CostLimit(1)}},
		{"CostLimit(1)+OptPartialEval", []cel.ProgramOption{cel.EvalOptions(cel.OptPartialEval), cel.CostLimit(1)}},
		{"OptTrackCost", []cel.ProgramOption{cel.EvalOptions(cel.OptTrackCost)}},
		{"OptTrackCost+OptTrackState", []cel.ProgramOption{cel.EvalOptions(cel.OptTrackCost | cel.OptTrackState)}},
	}

	for _, tc := range cases {
		prg, err := env.Program(ast, tc.opts...)
		if err != nil {
			panic(err)
		}
		_, det, evalErr := prg.Eval(map[string]any{"a": 42, "b": 4})
		cost := "<nil>"
		if det != nil && det.ActualCost() != nil {
			cost = fmt.Sprint(*det.ActualCost())
		}
		fmt.Printf("%-34s err=%-46v actualCost=%s\n", tc.name, evalErr, cost)
	}
}

Expected behavior

CostLimit(1)                       err=operation cancelled: actual cost limit exceeded actualCost=2
CostLimit(1)+OptTrackState         err=operation cancelled: actual cost limit exceeded actualCost=2
CostLimit(1)+OptExhaustiveEval     err=operation cancelled: actual cost limit exceeded actualCost=2
CostLimit(1)+OptPartialEval        err=operation cancelled: actual cost limit exceeded actualCost=2
OptTrackCost                       err=<nil>                                          actualCost=4
OptTrackCost+OptTrackState         err=<nil>                                          actualCost=4

Actual behavior

CostLimit(1)                       err=operation cancelled: actual cost limit exceeded actualCost=2
CostLimit(1)+OptTrackState         err=<nil>                                          actualCost=0   <-- BUG
CostLimit(1)+OptExhaustiveEval     err=<nil>                                          actualCost=0   <-- BUG
CostLimit(1)+OptPartialEval        err=operation cancelled: actual cost limit exceeded actualCost=2
OptTrackCost                       err=<nil>                                          actualCost=4
OptTrackCost+OptTrackState         err=<nil>                                          actualCost=0   <-- BUG

Additional context
First seen in https://github.com/authzed/spicedb/actions/runs/32164309872/job/95800380925?pr=3269#step:4:102

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions