Skip to content

Commit 297928e

Browse files
l46kokcopybara-github
authored andcommitted
Fix a fuzzer issue involving nested comprehensions
PiperOrigin-RevId: 746534921
1 parent 20bbd26 commit 297928e

File tree

6 files changed

+151
-13
lines changed

6 files changed

+151
-13
lines changed

.bazelrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ build --java_language_version=11
55

66
# Hide Java 8 deprecation warnings.
77
common --javacopt=-Xlint:-options
8+
9+
# MacOS Fix https://github.com/protocolbuffers/protobuf/issues/16944
10+
build --host_cxxopt=-std=c++14
11+
build --cxxopt=-std=c++14

runtime/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ java_library(
105105
exports = ["//runtime/src/main/java/dev/cel/runtime:interpreter"],
106106
)
107107

108+
java_library(
109+
name = "interpretable",
110+
visibility = ["//:internal"],
111+
exports = ["//runtime/src/main/java/dev/cel/runtime:interpretable"],
112+
)
113+
108114
java_library(
109115
name = "runtime_helpers",
110116
visibility = ["//:internal"],

runtime/src/main/java/dev/cel/runtime/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ cel_android_library(
328328
"//common/types:types_android",
329329
"@maven//:com_google_code_findbugs_annotations",
330330
"@maven//:com_google_errorprone_error_prone_annotations",
331+
"@maven//:com_google_guava_guava",
331332
"@maven_android//:com_google_guava_guava",
332333
],
333334
)
@@ -506,7 +507,6 @@ java_library(
506507
java_library(
507508
name = "interpretable",
508509
srcs = INTERPRABLE_SOURCES,
509-
visibility = ["//visibility:private"],
510510
deps = [
511511
":evaluation_exception",
512512
":evaluation_listener",

runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import static com.google.common.base.Preconditions.checkNotNull;
1818

1919
import com.google.auto.value.AutoValue;
20+
import com.google.common.annotations.VisibleForTesting;
2021
import com.google.common.base.Joiner;
2122
import com.google.common.base.Preconditions;
2223
import com.google.common.collect.ImmutableList;
24+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2325
import com.google.errorprone.annotations.Immutable;
2426
import javax.annotation.concurrent.ThreadSafe;
2527
import dev.cel.common.CelAbstractSyntaxTree;
@@ -51,9 +53,7 @@
5153
import java.util.Map;
5254
import java.util.Optional;
5355

54-
/**
55-
* Default implementation of the CEL interpreter.
56-
*/
56+
/** Default implementation of the CEL interpreter. */
5757
@ThreadSafe
5858
final class DefaultInterpreter implements Interpreter {
5959

@@ -111,8 +111,8 @@ public Interpretable createInterpretable(CelAbstractSyntaxTree ast) {
111111
}
112112

113113
@Immutable
114-
private static final class DefaultInterpretable
115-
implements Interpretable, UnknownTrackingInterpretable {
114+
@VisibleForTesting
115+
static final class DefaultInterpretable implements Interpretable, UnknownTrackingInterpretable {
116116
private final TypeResolver typeResolver;
117117
private final RuntimeTypeProvider typeProvider;
118118
private final Dispatcher.ImmutableCopy dispatcher;
@@ -165,14 +165,44 @@ public Object evalTrackingUnknowns(
165165
Optional<? extends FunctionResolver> functionResolver,
166166
CelEvaluationListener listener)
167167
throws CelEvaluationException {
168-
int comprehensionMaxIterations =
169-
celOptions.enableComprehension() ? celOptions.comprehensionMaxIterations() : 0;
170-
ExecutionFrame frame =
171-
new ExecutionFrame(listener, resolver, functionResolver, comprehensionMaxIterations);
168+
ExecutionFrame frame = newExecutionFrame(resolver, functionResolver, listener);
172169
IntermediateResult internalResult = evalInternal(frame, ast.getExpr());
173170
return internalResult.value();
174171
}
175172

173+
/**
174+
* Evaluates this interpretable and returns the resulting execution frame populated with
175+
* evaluation state. This method is specifically designed for testing the interpreter's internal
176+
* invariants.
177+
*
178+
* <p><b>Do not expose to public. This method is strictly for internal testing purposes
179+
* only.</b>
180+
*/
181+
@VisibleForTesting
182+
@CanIgnoreReturnValue
183+
ExecutionFrame populateExecutionFrame(ExecutionFrame frame) throws CelEvaluationException {
184+
evalInternal(frame, ast.getExpr());
185+
186+
return frame;
187+
}
188+
189+
@VisibleForTesting
190+
ExecutionFrame newTestExecutionFrame(GlobalResolver resolver) {
191+
return newExecutionFrame(
192+
RuntimeUnknownResolver.fromResolver(resolver),
193+
Optional.empty(),
194+
CelEvaluationListener.noOpListener());
195+
}
196+
197+
private ExecutionFrame newExecutionFrame(
198+
RuntimeUnknownResolver resolver,
199+
Optional<? extends FunctionResolver> functionResolver,
200+
CelEvaluationListener listener) {
201+
int comprehensionMaxIterations =
202+
celOptions.enableComprehension() ? celOptions.comprehensionMaxIterations() : 0;
203+
return new ExecutionFrame(listener, resolver, functionResolver, comprehensionMaxIterations);
204+
}
205+
176206
private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr)
177207
throws CelEvaluationException {
178208
try {
@@ -927,8 +957,12 @@ private IntermediateResult evalComprehension(
927957
}
928958

929959
frame.pushScope(Collections.singletonMap(accuVar, accuValue));
930-
IntermediateResult result = evalInternal(frame, compre.result());
931-
frame.popScope();
960+
IntermediateResult result;
961+
try {
962+
result = evalInternal(frame, compre.result());
963+
} finally {
964+
frame.popScope();
965+
}
932966
return result;
933967
}
934968

@@ -976,13 +1010,14 @@ private LazyExpression(CelExpr celExpr) {
9761010
}
9771011

9781012
/** This class tracks the state meaningful to a single evaluation pass. */
979-
private static class ExecutionFrame {
1013+
static class ExecutionFrame {
9801014
private final CelEvaluationListener evaluationListener;
9811015
private final int maxIterations;
9821016
private final ArrayDeque<RuntimeUnknownResolver> resolvers;
9831017
private final Optional<? extends FunctionResolver> lateBoundFunctionResolver;
9841018
private RuntimeUnknownResolver currentResolver;
9851019
private int iterations;
1020+
@VisibleForTesting int scopeLevel;
9861021

9871022
private ExecutionFrame(
9881023
CelEvaluationListener evaluationListener,
@@ -1040,12 +1075,14 @@ private void cacheLazilyEvaluatedResult(
10401075

10411076
/** Note: we utilize a HashMap instead of ImmutableMap to make lookups faster on string keys. */
10421077
private void pushScope(Map<String, IntermediateResult> scope) {
1078+
scopeLevel++;
10431079
RuntimeUnknownResolver scopedResolver = currentResolver.withScope(scope);
10441080
currentResolver = scopedResolver;
10451081
resolvers.addLast(scopedResolver);
10461082
}
10471083

10481084
private void popScope() {
1085+
scopeLevel--;
10491086
if (resolvers.isEmpty()) {
10501087
throw new IllegalStateException("Execution frame error: more scopes popped than pushed");
10511088
}

runtime/src/test/java/dev/cel/runtime/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ java_library(
7272
"//common:cel_descriptors",
7373
"//common:cel_exception",
7474
"//common:cel_source",
75+
"//common:compiler_common",
7576
"//common:error_codes",
7677
"//common:options",
7778
"//common:proto_ast",
@@ -100,6 +101,8 @@ java_library(
100101
"//runtime:evaluation_listener",
101102
"//runtime:function_binding",
102103
"//runtime:function_overload_impl",
104+
"//runtime:interpretable",
105+
"//runtime:interpreter",
103106
"//runtime:interpreter_util",
104107
"//runtime:lite_runtime",
105108
"//runtime:lite_runtime_factory",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.junit.Assert.assertThrows;
19+
20+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
21+
import dev.cel.common.CelAbstractSyntaxTree;
22+
import dev.cel.common.CelFunctionDecl;
23+
import dev.cel.common.CelOptions;
24+
import dev.cel.common.CelOverloadDecl;
25+
import dev.cel.common.types.SimpleType;
26+
import dev.cel.compiler.CelCompiler;
27+
import dev.cel.compiler.CelCompilerFactory;
28+
import dev.cel.parser.CelStandardMacro;
29+
import dev.cel.runtime.DefaultInterpreter.DefaultInterpretable;
30+
import dev.cel.runtime.DefaultInterpreter.ExecutionFrame;
31+
import java.util.Map;
32+
import org.junit.Test;
33+
import org.junit.runner.RunWith;
34+
35+
/**
36+
* Exercises tests for the internals of the {@link DefaultInterpreter}. Tests should only be added
37+
* here if we absolutely must add coverage for internal state of the interpreter that's otherwise
38+
* impossible with the CEL public APIs.
39+
*/
40+
@RunWith(TestParameterInjector.class)
41+
public class DefaultInterpreterTest {
42+
43+
@Test
44+
public void nestedComprehensions_accuVarContainsErrors_scopeLevelInvariantNotViolated()
45+
throws Exception {
46+
CelCompiler celCompiler =
47+
CelCompilerFactory.standardCelCompilerBuilder()
48+
.addFunctionDeclarations(
49+
CelFunctionDecl.newFunctionDeclaration(
50+
"error", CelOverloadDecl.newGlobalOverload("error_overload", SimpleType.DYN)))
51+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
52+
.build();
53+
RuntimeTypeProvider emptyProvider =
54+
new RuntimeTypeProvider() {
55+
@Override
56+
public Object createMessage(String messageName, Map<String, Object> values) {
57+
return null;
58+
}
59+
60+
@Override
61+
public Object selectField(Object message, String fieldName) {
62+
return null;
63+
}
64+
65+
@Override
66+
public Object hasField(Object message, String fieldName) {
67+
return null;
68+
}
69+
70+
@Override
71+
public Object adapt(Object message) {
72+
return message;
73+
}
74+
};
75+
CelAbstractSyntaxTree ast = celCompiler.compile("[1].all(x, [2].all(y, error()))").getAst();
76+
DefaultDispatcher dispatcher = DefaultDispatcher.create();
77+
dispatcher.add("error", long.class, (args) -> new IllegalArgumentException("Always throws"));
78+
DefaultInterpreter defaultInterpreter =
79+
new DefaultInterpreter(new TypeResolver(), emptyProvider, dispatcher, CelOptions.DEFAULT);
80+
DefaultInterpretable interpretable =
81+
(DefaultInterpretable) defaultInterpreter.createInterpretable(ast);
82+
83+
ExecutionFrame frame = interpretable.newTestExecutionFrame(GlobalResolver.EMPTY);
84+
85+
assertThrows(CelEvaluationException.class, () -> interpretable.populateExecutionFrame(frame));
86+
assertThat(frame.scopeLevel).isEqualTo(0);
87+
}
88+
}

0 commit comments

Comments
 (0)