Skip to content

Commit 7ca44dc

Browse files
committed
Add resource limits
1 parent 52b1e03 commit 7ca44dc

72 files changed

Lines changed: 3502 additions & 69 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and failure handling.
2323
- [Authorization expressions](docs/guides/authorization.md)
2424
- [Custom functions](docs/guides/custom-functions.md)
2525
- [Advanced use](docs/index.md#advanced-use)
26+
- [Resource controls and cancellation](docs/advanced/resource-controls-and-cancellation.md)
2627
- [Internals and optimization](docs/index.md#internals)
2728
- [Optimization mechanisms](docs/internals/optimization-mechanisms.md)
2829
- [Native evaluation](docs/internals/native-evaluation.md)
@@ -141,12 +142,15 @@ generated Java, generated bytecode, JNI, or CPU-native compiled expressions. See
141142

142143
CEL restricts side effects and general recursion, but an expression can still be expensive for
143144
large inputs or through comprehensions, regular expressions, and custom functions. CEL-Java
144-
provides parser recursion and source-size limits; it does not currently provide a general
145-
evaluation time, step, memory, or comprehension-iteration budget.
146-
147-
Applications accepting untrusted expressions or inputs must apply their own admission policy,
148-
input-size limits, execution isolation or deadlines, and fail-closed handling appropriate to the
149-
use case. See [Compatibility and limitations](docs/reference/compatibility-and-limitations.md).
145+
provides cooperative cancellation, elapsed and executing-thread CPU/allocation budgets, structural
146+
AST admission, and parser recursion/source-size limits. These controls are not process isolation
147+
and do not preempt arbitrary host callbacks.
148+
149+
Applications accepting untrusted expressions or inputs should combine those controls with an
150+
admission policy, bounded inputs and functions, deployment isolation, and fail-closed handling
151+
appropriate to the use case. See
152+
[Resource controls and cancellation](docs/advanced/resource-controls-and-cancellation.md) and
153+
[Compatibility and limitations](docs/reference/compatibility-and-limitations.md).
150154

151155
Report vulnerabilities according to the [security policy](SECURITY.md).
152156

core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ public void envCompileAndProgram(CompileState state, Blackhole blackhole) {
8181
blackhole.consume(env.program(ast.getAst()));
8282
}
8383

84+
@Benchmark
85+
public void envControlledCompileAndProgram(CompileState state, Blackhole blackhole) {
86+
Env env =
87+
newEnv(
88+
declarations(
89+
Decls.newVar("resource", Decls.String),
90+
Decls.newVar("user", Decls.String),
91+
Decls.newVar("request", Decls.Dyn),
92+
Decls.newVar("items", Decls.newListType(Decls.Dyn))));
93+
AstIssuesTuple ast = env.compileCancelable(state.source()).execute();
94+
if (ast.hasIssues()) {
95+
throw ast.getIssues().err();
96+
}
97+
blackhole.consume(env.programCancelable(ast.getAst()).execute());
98+
}
99+
84100
@Benchmark
85101
public void scriptCompilerBuild(CompileState state, Blackhole blackhole) throws Exception {
86102
Script script =

core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
import static org.projectnessie.cel.interpreter.Activation.newActivation;
2424

2525
import dev.cel.expr.conformance.proto3.TestAllTypes;
26+
import java.time.Duration;
27+
import java.util.List;
2628
import java.util.Map;
2729
import java.util.concurrent.TimeUnit;
2830
import java.util.function.Supplier;
31+
import java.util.stream.LongStream;
2932
import org.openjdk.jmh.annotations.Benchmark;
3033
import org.openjdk.jmh.annotations.BenchmarkMode;
3134
import org.openjdk.jmh.annotations.Fork;
@@ -61,7 +64,8 @@ public static class EvaluationState {
6164
"chainString",
6265
"shortCircuit",
6366
"mapSelection",
64-
"protoSelection"
67+
"protoSelection",
68+
"longQuantifier"
6569
})
6670
public String expression;
6771

@@ -71,6 +75,9 @@ public static class EvaluationState {
7175
Activation activation;
7276
Class<?> nativeResultType;
7377
Supplier<Object> nativeJava;
78+
ResourceLimits elapsedLimit;
79+
ResourceLimits cpuLimit;
80+
ResourceLimits allocationLimit;
7481

7582
@Setup
7683
public void init() {
@@ -159,6 +166,14 @@ public void init() {
159166
nativeResultType = Boolean.class;
160167
nativeJava = () -> message.getSingleInt64() == (long) variables.get("target");
161168
break;
169+
case "longQuantifier":
170+
env = newEnv(declarations(Decls.newVar("values", Decls.newListType(Decls.Int))));
171+
source = "values.exists(x, x == -1)";
172+
List<Long> values = LongStream.range(0, 1_000).boxed().toList();
173+
variables = Map.of("values", values);
174+
nativeResultType = Boolean.class;
175+
nativeJava = () -> values.stream().noneMatch(value -> value == -1);
176+
break;
162177
default:
163178
throw new IllegalArgumentException(
164179
"Unknown evaluator benchmark expression: " + expression);
@@ -171,6 +186,9 @@ public void init() {
171186
program = env.program(ast.getAst(), evalOptions(OptOptimize));
172187
internalProgram = (Prog) program;
173188
activation = newActivation(variables);
189+
elapsedLimit = ResourceLimits.newBuilder().elapsedTimeLimit(Duration.ofSeconds(1)).build();
190+
cpuLimit = ResourceLimits.newBuilder().cpuTimeLimit(Duration.ofSeconds(1)).build();
191+
allocationLimit = ResourceLimits.newBuilder().allocatedBytesLimit(1_000_000).build();
174192
}
175193
}
176194

@@ -184,6 +202,26 @@ public Program.EvalResult programEval(EvaluationState state) {
184202
return state.program.eval(state.variables);
185203
}
186204

205+
@Benchmark
206+
public Program.EvalResult programEvalCancellationOnly(EvaluationState state) {
207+
return state.program.evalCancelable(state.variables).eval();
208+
}
209+
210+
@Benchmark
211+
public Program.EvalResult programEvalElapsedLimit(EvaluationState state) {
212+
return state.program.evalCancelable(state.variables, state.elapsedLimit).eval();
213+
}
214+
215+
@Benchmark
216+
public Program.EvalResult programEvalCpuLimit(EvaluationState state) {
217+
return state.program.evalCancelable(state.variables, state.cpuLimit).eval();
218+
}
219+
220+
@Benchmark
221+
public Program.EvalResult programEvalAllocationLimit(EvaluationState state) {
222+
return state.program.evalCancelable(state.variables, state.allocationLimit).eval();
223+
}
224+
187225
@Benchmark
188226
public Object programEvalNative(EvaluationState state) {
189227
return state.internalProgram.e.adapter.valueToNative(

core/src/main/congocc/cel/cel-java.ccc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ INCLUDE "cel.ccc"
2323

2424
INJECT PARSER_CLASS :
2525
{
26+
private void resourceCheckpoint() {
27+
org.projectnessie.cel.internal.OperationCheckpoints.currentController().checkpoint(
28+
org.projectnessie.cel.OperationAbortedException.Phase.PARSE);
29+
}
30+
2631
private boolean nextTokenStartsExpression() {
2732
TokenType type = getToken(1).getType();
2833
return switch (type) {

core/src/main/congocc/cel/cel.ccc

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
INCLUDE "cel-lexer.ccc"
1818

19-
Start : Expr! <EOF>! ;
19+
Start : { resourceCheckpoint(); } Expr! <EOF>! ;
2020

2121
Expr :
22+
{ resourceCheckpoint(); }
2223
ConditionalOr
2324
[<QUESTIONMARK> ConditionalOr <COLON> Expr]
2425
;
@@ -44,12 +45,13 @@ Multiplicative :
4445
;
4546

4647
Unary :
47-
(<EXCLAM> | <MINUS>)* Member
48+
({ resourceCheckpoint(); } (<EXCLAM> | <MINUS>))* Member
4849
;
4950

5051
Member :
5152
Primary
5253
(
54+
{ resourceCheckpoint(); }
5355
<DOT> [<QUESTIONMARK>] Field [<LPAREN> (<RPAREN> | ExprList <RPAREN>)]
5456
| <LBRACKET> [<QUESTIONMARK>] Expr <RBRACKET>
5557
| <LBRACE> [FieldInitializerList] [<COMMA>] <RBRACE>
@@ -65,15 +67,16 @@ Primary :
6567
;
6668

6769
ExprList :
68-
Expr (<COMMA> Expr =>||)*!
70+
Expr ({ resourceCheckpoint(); } <COMMA> Expr =>||)*!
6971
;
7072

7173
ListInitializerList :
72-
[<QUESTIONMARK>] Expr (<COMMA> [<QUESTIONMARK>] Expr =>||)*!
74+
[<QUESTIONMARK>] Expr ({ resourceCheckpoint(); } <COMMA> [<QUESTIONMARK>] Expr =>||)*!
7375
;
7476

7577
FieldInitializerList :
76-
[<QUESTIONMARK>] Field <COLON> Expr (<COMMA> [<QUESTIONMARK>] Field <COLON> Expr =>||)*!
78+
[<QUESTIONMARK>] Field <COLON> Expr
79+
({ resourceCheckpoint(); } <COMMA> [<QUESTIONMARK>] Field <COLON> Expr =>||)*!
7780
;
7881

7982
Field :
@@ -82,7 +85,8 @@ Field :
8285
;
8386

8487
MapInitializerList :
85-
[<QUESTIONMARK>] Expr <COLON> Expr (<COMMA> [<QUESTIONMARK>] Expr <COLON> Expr =>||)*!
88+
[<QUESTIONMARK>] Expr <COLON> Expr
89+
({ resourceCheckpoint(); } <COMMA> [<QUESTIONMARK>] Expr <COLON> Expr =>||)*!
8690
;
8791

8892
ConstantLiteral :
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (C) 2026 The Authors of CEL-Java
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.projectnessie.cel;
17+
18+
import com.google.api.expr.v1alpha1.Expr;
19+
import java.util.ArrayDeque;
20+
import org.projectnessie.cel.OperationAbortedException.Phase;
21+
import org.projectnessie.cel.OperationAbortedException.Reason;
22+
import org.projectnessie.cel.OperationAbortedException.Resource;
23+
import org.projectnessie.cel.internal.OperationController;
24+
25+
/** Iterative structural admission for controlled AST operations. */
26+
final class AstAdmission {
27+
private AstAdmission() {}
28+
29+
static void check(Ast ast, OperationController controller, Phase phase) {
30+
var limits = controller.limits();
31+
if (limits.astNodes() >= 0 || limits.astDepth() >= 0) {
32+
checkTree(ast.getExpr(), controller, phase, limits.astNodes(), limits.astDepth());
33+
}
34+
if (limits.astMetadataEntries() >= 0) {
35+
long entries = 0;
36+
var info = ast.getSourceInfo();
37+
if (info != null) {
38+
entries = add(entries, info.getPositionsCount(), limits.astMetadataEntries(), phase);
39+
entries = add(entries, info.getLineOffsetsCount(), limits.astMetadataEntries(), phase);
40+
entries = add(entries, info.getMacroCallsCount(), limits.astMetadataEntries(), phase);
41+
for (Expr macro : info.getMacroCallsMap().values()) {
42+
entries =
43+
checkMetadataTree(macro, controller, phase, entries, limits.astMetadataEntries());
44+
}
45+
}
46+
entries = add(entries, ast.refMap.size(), limits.astMetadataEntries(), phase);
47+
add(entries, ast.typeMap.size(), limits.astMetadataEntries(), phase);
48+
}
49+
}
50+
51+
private static long checkTree(
52+
Expr root, OperationController controller, Phase phase, long nodeLimit, int depthLimit) {
53+
var pending = new ArrayDeque<NodeDepth>();
54+
pending.push(new NodeDepth(root, 1));
55+
long count = 0;
56+
while (!pending.isEmpty()) {
57+
controller.checkpoint(phase);
58+
var current = pending.pop();
59+
count++;
60+
if (nodeLimit >= 0 && count > nodeLimit) {
61+
throw limit(Reason.AST_NODE_LIMIT, phase, Resource.AST_NODES, nodeLimit, count);
62+
}
63+
if (depthLimit >= 0 && current.depth > depthLimit) {
64+
throw limit(Reason.AST_DEPTH_LIMIT, phase, Resource.AST_DEPTH, depthLimit, current.depth);
65+
}
66+
pushChildren(current.expr, current.depth + 1, pending);
67+
}
68+
return count;
69+
}
70+
71+
private static long checkMetadataTree(
72+
Expr root,
73+
OperationController controller,
74+
Phase phase,
75+
long initialCount,
76+
long metadataLimit) {
77+
var pending = new ArrayDeque<Expr>();
78+
pending.push(root);
79+
long count = initialCount;
80+
while (!pending.isEmpty()) {
81+
controller.checkpoint(phase);
82+
var current = pending.pop();
83+
count = add(count, 1, metadataLimit, phase);
84+
pushMetadataChildren(current, pending);
85+
}
86+
return count;
87+
}
88+
89+
private static void pushChildren(Expr expr, int depth, ArrayDeque<NodeDepth> pending) {
90+
switch (expr.getExprKindCase()) {
91+
case SELECT_EXPR -> pending.push(new NodeDepth(expr.getSelectExpr().getOperand(), depth));
92+
case CALL_EXPR -> {
93+
var call = expr.getCallExpr();
94+
for (Expr arg : call.getArgsList()) {
95+
pending.push(new NodeDepth(arg, depth));
96+
}
97+
if (call.hasTarget()) {
98+
pending.push(new NodeDepth(call.getTarget(), depth));
99+
}
100+
}
101+
case LIST_EXPR -> {
102+
for (Expr element : expr.getListExpr().getElementsList()) {
103+
pending.push(new NodeDepth(element, depth));
104+
}
105+
}
106+
case STRUCT_EXPR -> {
107+
for (var entry : expr.getStructExpr().getEntriesList()) {
108+
pending.push(new NodeDepth(entry.getValue(), depth));
109+
if (entry.hasMapKey()) {
110+
pending.push(new NodeDepth(entry.getMapKey(), depth));
111+
}
112+
}
113+
}
114+
case COMPREHENSION_EXPR -> {
115+
var comprehension = expr.getComprehensionExpr();
116+
pending.push(new NodeDepth(comprehension.getIterRange(), depth));
117+
pending.push(new NodeDepth(comprehension.getAccuInit(), depth));
118+
pending.push(new NodeDepth(comprehension.getLoopCondition(), depth));
119+
pending.push(new NodeDepth(comprehension.getLoopStep(), depth));
120+
pending.push(new NodeDepth(comprehension.getResult(), depth));
121+
}
122+
default -> {
123+
// Scalar expression.
124+
}
125+
}
126+
}
127+
128+
private static void pushMetadataChildren(Expr expr, ArrayDeque<Expr> pending) {
129+
switch (expr.getExprKindCase()) {
130+
case SELECT_EXPR -> pending.push(expr.getSelectExpr().getOperand());
131+
case CALL_EXPR -> {
132+
var call = expr.getCallExpr();
133+
pending.addAll(call.getArgsList());
134+
if (call.hasTarget()) {
135+
pending.push(call.getTarget());
136+
}
137+
}
138+
case LIST_EXPR -> pending.addAll(expr.getListExpr().getElementsList());
139+
case STRUCT_EXPR -> {
140+
for (var entry : expr.getStructExpr().getEntriesList()) {
141+
pending.push(entry.getValue());
142+
if (entry.hasMapKey()) {
143+
pending.push(entry.getMapKey());
144+
}
145+
}
146+
}
147+
case COMPREHENSION_EXPR -> {
148+
var comprehension = expr.getComprehensionExpr();
149+
pending.push(comprehension.getIterRange());
150+
pending.push(comprehension.getAccuInit());
151+
pending.push(comprehension.getLoopCondition());
152+
pending.push(comprehension.getLoopStep());
153+
pending.push(comprehension.getResult());
154+
}
155+
default -> {
156+
// Scalar expression.
157+
}
158+
}
159+
}
160+
161+
private static long add(long value, long increment, long limit, Phase phase) {
162+
var observed = value + increment;
163+
if (observed < value) {
164+
observed = Long.MAX_VALUE;
165+
}
166+
if (observed > limit) {
167+
throw limit(Reason.AST_METADATA_LIMIT, phase, Resource.AST_METADATA_ENTRIES, limit, observed);
168+
}
169+
return observed;
170+
}
171+
172+
private static OperationAbortedException limit(
173+
Reason reason, Phase phase, Resource resource, long limit, long observed) {
174+
return new OperationAbortedException(reason, phase, resource, limit, observed);
175+
}
176+
177+
private record NodeDepth(Expr expr, int depth) {}
178+
}

0 commit comments

Comments
 (0)