Skip to content

A CEL rule whose compile fails for want of stack stays cached, so every later validation of its type fails #204

Open

Description

@rinarakaki

Versions: @bufbuild/protovalidate 1.3.0 (the latest release; main has the same code), @bufbuild/protobuf 2.16.0, Node 26.11.1.

What happens. CelManager.compileRule (packages/protovalidate/src/cel.ts) catches any error that parse or plan throws and returns it as a compilation_error result. Planner.plan (planner.ts) caches the evaluator it builds for a message type in messageCache. Its try/catch drops that entry only when planning throws, but a rule that failed to compile is returned, not thrown, so the evaluator holding the failed rule stays cached.

So when the first validation of a type runs with the JavaScript stack nearly out, as when a deep caller validates, compiling one of its rules can fail for want of stack, and that failure is kept as the rule's CompilationError. Every later validate() of the type on that validator returns the same error, though the stack is free by then. In V8 the failure isn't even a RangeError: building a regular expression with the stack nearly out throws SyntaxError: Invalid regular expression: /^[rR]/g: Stack overflow.

Predefined rules, compiled through compileRules and cached in rulesCache, look to be kept the same way.

Repro. Use a message type with several CEL rules. Whether a given depth fails in compiling rather than elsewhere depends on the type's rules, so this tries a new validator at each depth as the stack unwinds, and stops at the first whose compile fails.

import { create } from "@bufbuild/protobuf";
import { CompilationError, createValidator, type Validator } from "@bufbuild/protovalidate";
import { ExampleSchema } from "./gen/example_pb"; // a message type with several CEL rules

const message = create(ExampleSchema, {});
let found: { validator: Validator; first: unknown } | undefined;
const deep = (): void => {
  try {
    deep();
  } catch (error) {
    if (found === undefined) {
      const validator = createValidator();
      try {
        const result = validator.validate(ExampleSchema, message);
        if (result.kind === "error" && result.error instanceof CompilationError) found = { validator, first: result };
      } catch {
        // not enough stack even to start; a shallower frame tries
      }
    }
    throw error;
  }
};
try {
  deep();
} catch {
  // the recursion's own RangeError
}

console.log(found?.first); // { kind: "error", error: CompilationError: failed to compile …: SyntaxError: Invalid regular expression: /^[rR]/g: Stack overflow }
console.log(found?.validator.validate(ExampleSchema, message)); // the same error, with the stack free
console.log(createValidator().validate(ExampleSchema, message)); // a new validator compiles it: { kind: "invalid", … }

With one of our types, a record with five message-level CEL rules and standard rules on many of its fields, that prints:

first:  CompilationError: failed to compile change.follows: SyntaxError: Invalid regular expression: /^[rR]/g: Stack overflow
later:  CompilationError: failed to compile change.follows: SyntaxError: Invalid regular expression: /^[rR]/g: Stack overflow
fresh:  invalid

Expected. A failure that doesn't come from the rule itself, such as running out of stack, isn't kept as the rule's compile result, so the next validation compiles the rule again. For instance, plan could keep a type's evaluator only once all of its rules compiled, or compileRule could rethrow what isn't about the expression, so that plan's catch drops the entry.

Workaround. When a validation fails with a CompilationError, make a new validator and validate again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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