From 898643129e3652efd43e596f978d99f85e770235 Mon Sep 17 00:00:00 2001 From: antmhs Date: Fri, 13 Mar 2026 19:19:57 +0200 Subject: [PATCH] feat: make policy length limits configurable per engine (#624) - Add PolicyLengthConfig struct with max_col, max_file_bytes, and max_lines fields, replacing hardcoded constants in the lexer. - Add Engine::set_policy_length_config and clear_policy_length_config to allow callers to override the default limits. - Add Source::from_contents_with_limits and from_file_with_limits for direct Source construction with custom limits; existing from_contents and from_file signatures are preserved using defaults. - Add tests for default rejection, custom limits, and engine plumbing. - Add bindings for C, C++, Python, WASM/JS, Java, Ruby, C#, Go --- bindings/c/main.c | 7 ++ bindings/cpp/main.cpp | 7 ++ bindings/cpp/regorus.hpp | 10 +- bindings/csharp/Regorus/Engine.cs | 18 ++++ bindings/csharp/Regorus/NativeMethods.cs | 23 ++++ bindings/csharp/Regorus/PolicyLengthConfig.cs | 53 ++++++++++ bindings/csharp/TestApp/Program.cs | 2 + bindings/ffi/src/engine.rs | 32 ++++++ bindings/ffi/src/limits.rs | 27 ++++- bindings/go/main.go | 2 + bindings/go/pkg/regorus/mod.go | 29 +++++ bindings/java/Test.java | 4 + bindings/java/src/lib.rs | 34 ++++++ .../java/com/microsoft/regorus/Engine.java | 18 ++++ .../microsoft/regorus/PolicyLengthConfig.java | 50 +++++++++ bindings/python/src/lib.rs | 30 ++++++ bindings/python/test.py | 2 + bindings/ruby/Cargo.lock | 1 + bindings/ruby/ext/regorusrb/Cargo.toml | 1 + bindings/ruby/ext/regorusrb/src/lib.rs | 100 ++++++++++++++---- bindings/ruby/test/test_regorus.rb | 5 + bindings/wasm/Cargo.lock | 12 +++ bindings/wasm/Cargo.toml | 1 + bindings/wasm/src/lib.rs | 32 ++++++ bindings/wasm/test.js | 3 + src/engine.rs | 72 ++++++++++++- src/lexer.rs | 71 +++++++++---- src/lib.rs | 1 + src/parser.rs | 5 + src/utils/limits/length.rs | 36 +++++++ src/utils/limits/mod.rs | 4 + tests/lexer/mod.rs | 87 +++++++++++++++ 32 files changed, 726 insertions(+), 53 deletions(-) create mode 100644 bindings/csharp/Regorus/PolicyLengthConfig.cs create mode 100644 bindings/java/src/main/java/com/microsoft/regorus/PolicyLengthConfig.java create mode 100644 src/utils/limits/length.rs diff --git a/bindings/c/main.c b/bindings/c/main.c index 887adc8..e9f01ee 100644 --- a/bindings/c/main.c +++ b/bindings/c/main.c @@ -11,6 +11,13 @@ int main() { if (r.status != Ok) goto error; + // Raise the default col limit to 2000 + RegorusPolicyLengthConfig len_config = { .max_col = 2000, .max_file_bytes = 1048576, .max_lines = 20000 }; + r = regorus_engine_set_policy_length_config(engine, len_config); + if (r.status != Ok) + goto error; + regorus_result_drop(r); + // Load policies. r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/framework.rego"); if (r.status != Ok) diff --git a/bindings/cpp/main.cpp b/bindings/cpp/main.cpp index 2596d1c..63cd92e 100644 --- a/bindings/cpp/main.cpp +++ b/bindings/cpp/main.cpp @@ -8,6 +8,13 @@ void example() engine.set_rego_v0(true); engine.set_enable_coverage(true); + + RegorusPolicyLengthConfig len_config; + // Raise the default col limit to 2000 + len_config.max_col = 2000; + len_config.max_file_bytes = 1048576; + len_config.max_lines = 20000; + engine.set_policy_length_config(len_config); // Add policies. engine.add_policy("objects.rego",R"(package objects diff --git a/bindings/cpp/regorus.hpp b/bindings/cpp/regorus.hpp index 6cdfb1c..5d43d70 100644 --- a/bindings/cpp/regorus.hpp +++ b/bindings/cpp/regorus.hpp @@ -131,7 +131,15 @@ namespace regorus { Result get_coverage_report_pretty() { return Result(regorus_engine_get_coverage_report_pretty(engine)); } - + + Result set_policy_length_config(RegorusPolicyLengthConfig config) { + return Result(regorus_engine_set_policy_length_config(engine, config)); + } + + Result clear_policy_length_config() { + return Result(regorus_engine_clear_policy_length_config(engine)); + } + ~Engine() { regorus_engine_drop(engine); } diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index 613f8dc..78d14de 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -82,6 +82,24 @@ namespace Regorus CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr)); }); } + + public void SetPolicyLengthConfig(PolicyLengthConfig config) + { + var nativeConfig = config.ToNative(); + UseHandle(enginePtr => + { + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr, nativeConfig)); + }); + } + + public void ClearPolicyLengthConfig() + { + UseHandle(enginePtr => + { + CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr)); + }); + } + public string? AddPolicy(string path, string rego) { return Utf8Marshaller.WithUtf8(path, pathPtr => diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index 8042450..df2170d 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -428,6 +428,18 @@ namespace Regorus.Internal [DllImport(LibraryName, EntryPoint = "regorus_engine_clear_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern RegorusResult regorus_engine_clear_execution_timer_config(RegorusEngine* engine); + /// + /// Set the policy length limits for a specific engine instance. + /// + [DllImport(LibraryName, EntryPoint = "regorus_engine_set_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_set_policy_length_config(RegorusEngine* engine, RegorusPolicyLengthConfig config); + + /// + /// Clear the policy length configuration for a specific engine instance. + /// + [DllImport(LibraryName, EntryPoint = "regorus_engine_clear_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_clear_policy_length_config(RegorusEngine* engine); + #endregion #region Execution Timer Global Methods @@ -772,6 +784,17 @@ namespace Regorus.Internal public uint check_interval; } + /// + /// FFI representation of the policy length configuration. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct RegorusPolicyLengthConfig + { + public uint max_col; + public UIntPtr max_file_bytes; + public UIntPtr max_lines; + } + /// /// Byte buffer returned from FFI. /// diff --git a/bindings/csharp/Regorus/PolicyLengthConfig.cs b/bindings/csharp/Regorus/PolicyLengthConfig.cs new file mode 100644 index 0000000..0b06a27 --- /dev/null +++ b/bindings/csharp/Regorus/PolicyLengthConfig.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Regorus +{ + /// + /// Policy source length limits enforced when loading policy files. + /// + public readonly struct PolicyLengthConfig + { + /// + /// Initializes a new instance of the struct. + /// + /// Maximum column width per line. Must be non-zero. + /// Maximum policy file size in bytes. Must be non-zero. + /// Maximum number of lines per policy file. Must be non-zero. + /// Thrown when any parameter is zero. + public PolicyLengthConfig(uint maxCol, nuint maxFileBytes, nuint maxLines) + { + if (maxCol == 0) + throw new ArgumentOutOfRangeException(nameof(maxCol), "Must be non-zero."); + if (maxFileBytes == 0) + throw new ArgumentOutOfRangeException(nameof(maxFileBytes), "Must be non-zero."); + if (maxLines == 0) + throw new ArgumentOutOfRangeException(nameof(maxLines), "Must be non-zero."); + + MaxCol = maxCol; + MaxFileBytes = maxFileBytes; + MaxLines = maxLines; + } + + /// Maximum column width per line (default: 1024). + public uint MaxCol { get; } + + /// Maximum policy file size in bytes (default: 1 MiB). + public nuint MaxFileBytes { get; } + + /// Maximum number of lines per policy file (default: 20000). + public nuint MaxLines { get; } + + internal Regorus.Internal.RegorusPolicyLengthConfig ToNative() + { + return new Regorus.Internal.RegorusPolicyLengthConfig + { + max_col = MaxCol, + max_file_bytes = MaxFileBytes, + max_lines = MaxLines, + }; + } + } +} diff --git a/bindings/csharp/TestApp/Program.cs b/bindings/csharp/TestApp/Program.cs index c9b638c..1cfb42f 100644 --- a/bindings/csharp/TestApp/Program.cs +++ b/bindings/csharp/TestApp/Program.cs @@ -20,6 +20,8 @@ w.Restart(); var engine = new Regorus.Engine(); engine.SetRegoV0(true); +// Raise the default col limit to 2000 +engine.SetPolicyLengthConfig(new Regorus.PolicyLengthConfig(maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000)); w.Stop(); var newEngineTicks = w.ElapsedTicks; diff --git a/bindings/ffi/src/engine.rs b/bindings/ffi/src/engine.rs index 817d41e..8079b8e 100644 --- a/bindings/ffi/src/engine.rs +++ b/bindings/ffi/src/engine.rs @@ -6,6 +6,7 @@ use crate::common::{ }; use crate::compiled_policy::RegorusCompiledPolicy; use crate::limits::RegorusExecutionTimerConfig; +use crate::limits::RegorusPolicyLengthConfig; use crate::lock::{new_handle, read, try_read, try_write, Handle, ReadGuard, WriteGuard}; use crate::panic_guard::with_unwind_guard; use alloc::boxed::Box; @@ -491,6 +492,37 @@ pub extern "C" fn regorus_engine_clear_execution_timer_config( }()) } +/// Set the policy length limits used when loading policies. +#[no_mangle] +pub extern "C" fn regorus_engine_set_policy_length_config( + engine: *mut RegorusEngine, + config: RegorusPolicyLengthConfig, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let engine = to_ref(engine)?; + let mut guard = engine.try_write()?; + guard.set_policy_length_config(config.to_policy_length_config()?); + Ok(()) + }()) + }) +} + +/// Clear the policy length configuration, reverting to defaults. +#[no_mangle] +pub extern "C" fn regorus_engine_clear_policy_length_config( + engine: *mut RegorusEngine, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let engine = to_ref(engine)?; + let mut guard = engine.try_write()?; + guard.clear_policy_length_config(); + Ok(()) + }()) + }) +} + /// Get pretty printed coverage report. /// /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty diff --git a/bindings/ffi/src/limits.rs b/bindings/ffi/src/limits.rs index f9ef407..18b74ff 100644 --- a/bindings/ffi/src/limits.rs +++ b/bindings/ffi/src/limits.rs @@ -4,7 +4,7 @@ use crate::common::{to_regorus_result, RegorusResult, RegorusStatus}; use alloc::format; use anyhow::{anyhow, Result}; -use core::num::NonZeroU32; +use core::num::{NonZeroU32, NonZeroUsize}; use core::time::Duration; use regorus::utils::limits::{self, ExecutionTimerConfig}; @@ -158,6 +158,31 @@ impl RegorusExecutionTimerConfig { } } +/// FFI representation of [`regorus::PolicyLengthConfig`]. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RegorusPolicyLengthConfig { + /// Maximum column width per line (must be non-zero). + pub max_col: u32, + /// Maximum policy file size in bytes (must be non-zero). + pub max_file_bytes: usize, + /// Maximum number of lines per policy file (must be non-zero). + pub max_lines: usize, +} + +impl RegorusPolicyLengthConfig { + pub fn to_policy_length_config(self) -> Result { + Ok(regorus::PolicyLengthConfig { + max_col: NonZeroU32::new(self.max_col) + .ok_or_else(|| anyhow!("max_col must be non-zero"))?, + max_file_bytes: NonZeroUsize::new(self.max_file_bytes) + .ok_or_else(|| anyhow!("max_file_bytes must be non-zero"))?, + max_lines: NonZeroUsize::new(self.max_lines) + .ok_or_else(|| anyhow!("max_lines must be non-zero"))?, + }) + } +} + #[no_mangle] pub extern "C" fn regorus_set_fallback_execution_timer_config( config: RegorusExecutionTimerConfig, diff --git a/bindings/go/main.go b/bindings/go/main.go index 5cca2db..e59de0d 100644 --- a/bindings/go/main.go +++ b/bindings/go/main.go @@ -18,6 +18,8 @@ func main() { defer engine.Close() engine.SetRegoV0(true) + // Raise the default col limit to 2000 + engine.SetPolicyLengthConfig(regorus.PolicyLengthConfig{MaxCol: 2000, MaxFileBytes: 1048576, MaxLines: 20000}) elapsed1 := time.Since(t) diff --git a/bindings/go/pkg/regorus/mod.go b/bindings/go/pkg/regorus/mod.go index 3e2a498..98aaf49 100644 --- a/bindings/go/pkg/regorus/mod.go +++ b/bindings/go/pkg/regorus/mod.go @@ -214,3 +214,32 @@ func (e *Engine) TakePrints() (string, error) { return C.GoString(result.output), nil } + +type PolicyLengthConfig struct { + MaxCol uint32 + MaxFileBytes uint + MaxLines uint +} + +func (e *Engine) SetPolicyLengthConfig(config PolicyLengthConfig) error { + c := C.RegorusPolicyLengthConfig{ + max_col: C.uint32_t(config.MaxCol), + max_file_bytes: C.size_t(config.MaxFileBytes), + max_lines: C.size_t(config.MaxLines), + } + result := C.regorus_engine_set_policy_length_config(e.e, c) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} + +func (e *Engine) ClearPolicyLengthConfig() error { + result := C.regorus_engine_clear_policy_length_config(e.e) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} diff --git a/bindings/java/Test.java b/bindings/java/Test.java index af482e7..08f2371 100644 --- a/bindings/java/Test.java +++ b/bindings/java/Test.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. import com.microsoft.regorus.Engine; +import com.microsoft.regorus.PolicyLengthConfig; import com.microsoft.regorus.PolicyModule; import com.microsoft.regorus.Program; import com.microsoft.regorus.Rvm; @@ -26,6 +27,9 @@ public class Test { // Enable coverage. engine.setEnableCoverage(true); + // Raise the default col limit to 2000 + engine.setPolicyLengthConfig(new PolicyLengthConfig(2000, 1048576, 20000)); + // Evaluate rule. String valueJson = engine.evalRule("data.test.message"); System.out.println(valueJson); diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 20e0672..6732c39 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use anyhow::Result; +use core::num::{NonZeroU32, NonZeroUsize}; use jni::objects::{JBooleanArray, JByteArray, JClass, JObject, JObjectArray, JString}; use jni::sys::{jboolean, jbooleanArray, jbyteArray, jlong, jobjectArray, jstring}; use jni::JNIEnv; @@ -365,6 +366,39 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_getAstAsJson( } } +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetPolicyLengthConfig( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + max_col: u32, + max_file_bytes: jlong, + max_lines: jlong, +) { + let _ = throw_err(env, |_env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + engine.set_policy_length_config(regorus::PolicyLengthConfig { + max_col: NonZeroU32::new(max_col) + .ok_or_else(|| anyhow::anyhow!("maxCol must be non-zero"))?, + max_file_bytes: NonZeroUsize::new(max_file_bytes as usize) + .ok_or_else(|| anyhow::anyhow!("maxFileBytes must be non-zero"))?, + max_lines: NonZeroUsize::new(max_lines as usize) + .ok_or_else(|| anyhow::anyhow!("maxLines must be non-zero"))?, + }); + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearPolicyLengthConfig( + _env: JNIEnv, + _class: JClass, + engine_ptr: jlong, +) { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + engine.clear_policy_length_config(); +} + #[no_mangle] pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine( _env: JNIEnv, diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java index 9ee6f47..765c1de 100644 --- a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java +++ b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java @@ -39,6 +39,8 @@ public class Engine implements AutoCloseable, Cloneable { private static native void nativeClearCoverageData(long enginePtr); private static native void nativeSetGatherPrints(long enginePtr, boolean b); private static native String nativeTakePrints(long enginePtr); + private static native void nativeSetPolicyLengthConfig(long enginePtr, int maxCol, long maxFileBytes, long maxLines); + private static native void nativeClearPolicyLengthConfig(long enginePtr); private static native void nativeDestroyEngine(long enginePtr); // Pointer to Engine allocated on Rust's heap, all native methods works on @@ -259,6 +261,22 @@ public class Engine implements AutoCloseable, Cloneable { return nativeTakePrints(enginePtr); } + /** + * Set the policy length limits used when loading policies. + * + * @param config Policy length configuration. + */ + public void setPolicyLengthConfig(PolicyLengthConfig config) { + nativeSetPolicyLengthConfig(enginePtr, config.maxCol, config.maxFileBytes, config.maxLines); + } + + /** + * Clear the policy length configuration, reverting to defaults. + */ + public void clearPolicyLengthConfig() { + nativeClearPolicyLengthConfig(enginePtr); + } + long getPtr() { return enginePtr; } diff --git a/bindings/java/src/main/java/com/microsoft/regorus/PolicyLengthConfig.java b/bindings/java/src/main/java/com/microsoft/regorus/PolicyLengthConfig.java new file mode 100644 index 0000000..f99a350 --- /dev/null +++ b/bindings/java/src/main/java/com/microsoft/regorus/PolicyLengthConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ + +package com.microsoft.regorus; + +/** + * Policy source length limits enforced when loading policy files. + * + * All values must be positive (non-zero). + */ +public final class PolicyLengthConfig { + /** + * Maximum column width per line (default: 1024). + */ + public final int maxCol; + + /** + * Maximum policy file size in bytes (default: 1 MiB). + */ + public final long maxFileBytes; + + /** + * Maximum number of lines per policy file (default: 20000). + */ + public final long maxLines; + + /** + * Create a new policy length configuration. + * + * @param maxCol Maximum column width per line. + * @param maxFileBytes Maximum policy file size in bytes. + * @param maxLines Maximum number of lines per policy file. + */ + public PolicyLengthConfig(int maxCol, long maxFileBytes, long maxLines) { + if (maxCol <= 0) { + throw new IllegalArgumentException("maxCol must be positive"); + } + if (maxFileBytes <= 0) { + throw new IllegalArgumentException("maxFileBytes must be positive"); + } + if (maxLines <= 0) { + throw new IllegalArgumentException("maxLines must be positive"); + } + this.maxCol = maxCol; + this.maxFileBytes = maxFileBytes; + this.maxLines = maxLines; + } +} diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 0e327d3..e97362b 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use anyhow::{anyhow, Result}; +use core::num::{NonZeroU32, NonZeroUsize}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::*; @@ -392,6 +393,35 @@ impl Engine { .add_extension(path, nargs, Box::new(extension_impl)) } + /// Set the policy length limits used when loading policies. + /// + /// * `max_col`: Maximum column width per line. + /// * `max_file_bytes`: Maximum policy file size in bytes. + /// * `max_lines`: Maximum number of lines per policy file. + #[pyo3(signature = (*, max_col, max_file_bytes, max_lines))] + pub fn set_policy_length_config( + &mut self, + max_col: u32, + max_file_bytes: usize, + max_lines: usize, + ) -> Result<()> { + self.engine + .set_policy_length_config(::regorus::PolicyLengthConfig { + max_col: NonZeroU32::new(max_col) + .ok_or_else(|| anyhow!("max_col must be non-zero"))?, + max_file_bytes: NonZeroUsize::new(max_file_bytes) + .ok_or_else(|| anyhow!("max_file_bytes must be non-zero"))?, + max_lines: NonZeroUsize::new(max_lines) + .ok_or_else(|| anyhow!("max_lines must be non-zero"))?, + }); + Ok(()) + } + + /// Clear the policy length configuration, reverting to defaults. + pub fn clear_policy_length_config(&mut self) { + self.engine.clear_policy_length_config(); + } + /// Enable code coverage /// /// * `enable`: Whether to enable coverage or not. diff --git a/bindings/python/test.py b/bindings/python/test.py index aef2e18..2f6262e 100644 --- a/bindings/python/test.py +++ b/bindings/python/test.py @@ -10,6 +10,8 @@ if hasattr(sys.stdout, "reconfigure"): engine = regorus.Engine() engine.set_rego_v0(True) +# Raise the default col limit to 2000 +engine.set_policy_length_config(max_col=2000, max_file_bytes=1048576, max_lines=20000) # Load policies pkg = engine.add_policy_from_file('../../tests/aci/framework.rego') diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock index 0cd8f85..92d4882 100644 --- a/bindings/ruby/Cargo.lock +++ b/bindings/ruby/Cargo.lock @@ -962,6 +962,7 @@ version = "0.9.1" dependencies = [ "magnus", "regorus", + "serde", "serde_json", "serde_magnus", ] diff --git a/bindings/ruby/ext/regorusrb/Cargo.toml b/bindings/ruby/ext/regorusrb/Cargo.toml index fac8f2e..7d7de82 100644 --- a/bindings/ruby/ext/regorusrb/Cargo.toml +++ b/bindings/ruby/ext/regorusrb/Cargo.toml @@ -18,5 +18,6 @@ coverage = ["regorus/coverage"] [dependencies] magnus = { version = "0.8.2" } regorus = { path = "../../../..", default-features = false, features = ["arc"] } +serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" serde_magnus = "0.11.0" diff --git a/bindings/ruby/ext/regorusrb/src/lib.rs b/bindings/ruby/ext/regorusrb/src/lib.rs index 615ce0e..c4142a4 100644 --- a/bindings/ruby/ext/regorusrb/src/lib.rs +++ b/bindings/ruby/ext/regorusrb/src/lib.rs @@ -1,10 +1,19 @@ +use core::num::{NonZeroU32, NonZeroUsize}; use magnus::{Error, Ruby, exception::runtime_error, method, module, prelude::*}; use regorus::Engine as RegorusEngine; +use serde::Deserialize; use std::cell::RefCell; use std::cmp::Ordering; // `Value` exists under magnus, regorus, and serde_json, so be explicit +#[derive(Deserialize)] +struct PolicyLengthSpec { + max_col: u32, + max_file_bytes: usize, + max_lines: usize, +} + #[derive(Default)] #[magnus::wrap(class = "Regorus::Engine")] pub struct Engine { @@ -55,15 +64,17 @@ impl Engine { .map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {e}"))) } - fn add_data(&self, ruby_hash: magnus::RHash) -> Result<(), Error> { - let data_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| { - Error::new( - runtime_error(), - format!("Failed to deserialize Ruby value: {e}"), - ) - })?; + fn add_data(ruby: &Ruby, rb_self: &Self, ruby_hash: magnus::RHash) -> Result<(), Error> { + let data_value: regorus::Value = + serde_magnus::deserialize(ruby, ruby_hash).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to deserialize Ruby value: {e}"), + ) + })?; - self.engine + rb_self + .engine .borrow_mut() .add_data(data_value) .map_err(|e| Error::new(runtime_error(), format!("Failed to add data: {e}"))) @@ -111,15 +122,16 @@ impl Engine { .map_err(|e| Error::new(runtime_error(), format!("Failed to get policies: {e}"))) } - fn set_input(&self, ruby_hash: magnus::RHash) -> Result<(), Error> { - let input_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| { - Error::new( - runtime_error(), - format!("Failed to deserialize Ruby value: {e}"), - ) - })?; + fn set_input(ruby: &Ruby, rb_self: &Self, ruby_hash: magnus::RHash) -> Result<(), Error> { + let input_value: regorus::Value = + serde_magnus::deserialize(ruby, ruby_hash).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to deserialize Ruby value: {e}"), + ) + })?; - self.engine.borrow_mut().set_input(input_value); + rb_self.engine.borrow_mut().set_input(input_value); Ok(()) } @@ -142,14 +154,14 @@ impl Engine { Ok(()) } - fn eval_query(&self, query: String) -> Result { - let results = self + fn eval_query(ruby: &Ruby, rb_self: &Self, query: String) -> Result { + let results = rb_self .engine .borrow_mut() .eval_query(query, false) .map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {e}")))?; - serde_magnus::serialize(&results).map_err(|e| { + serde_magnus::serialize(ruby, &results).map_err(|e| { Error::new( runtime_error(), format!("Failed to serailzie query results: {e}"), @@ -177,15 +189,19 @@ impl Engine { }) } - fn eval_rule(&self, query: String) -> Result, Error> { + fn eval_rule( + ruby: &Ruby, + rb_self: &Self, + query: String, + ) -> Result, Error> { let result = - self.engine.borrow_mut().eval_rule(query).map_err(|e| { + rb_self.engine.borrow_mut().eval_rule(query).map_err(|e| { Error::new(runtime_error(), format!("Failed to evaluate rule: {e}")) })?; match result { - regorus::Value::Undefined => Ok(None), // Convert undefined to Ruby's nil - _ => serde_magnus::serialize(&result) // Serialize other results normally + regorus::Value::Undefined => Ok(None), + _ => serde_magnus::serialize(ruby, &result) .map(Some) .map_err(|e| { magnus::Error::new( @@ -280,6 +296,34 @@ impl Engine { }) } + fn set_policy_length_config( + ruby: &Ruby, + rb_self: &Self, + hash: magnus::RHash, + ) -> Result<(), Error> { + let spec: PolicyLengthSpec = serde_magnus::deserialize(ruby, hash).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to deserialize policy length config: {e}"), + ) + })?; + let config = regorus::PolicyLengthConfig { + max_col: NonZeroU32::new(spec.max_col) + .ok_or_else(|| Error::new(runtime_error(), "max_col must be non-zero"))?, + max_file_bytes: NonZeroUsize::new(spec.max_file_bytes) + .ok_or_else(|| Error::new(runtime_error(), "max_file_bytes must be non-zero"))?, + max_lines: NonZeroUsize::new(spec.max_lines) + .ok_or_else(|| Error::new(runtime_error(), "max_lines must be non-zero"))?, + }; + rb_self.engine.borrow_mut().set_policy_length_config(config); + Ok(()) + } + + fn clear_policy_length_config(&self) -> Result<(), Error> { + self.engine.borrow_mut().clear_policy_length_config(); + Ok(()) + } + #[cfg(feature = "ast")] fn get_ast_as_json(&self) -> Result { self.engine @@ -361,6 +405,16 @@ fn init(ruby: &Ruby) -> Result<(), Error> { engine_class.define_method("set_gather_prints", method!(Engine::set_gather_prints, 1))?; engine_class.define_method("take_prints", method!(Engine::take_prints, 0))?; + // policy length limits + engine_class.define_method( + "set_policy_length_config", + method!(Engine::set_policy_length_config, 1), + )?; + engine_class.define_method( + "clear_policy_length_config", + method!(Engine::clear_policy_length_config, 0), + )?; + // ast engine_class.define_method("get_ast_as_json", method!(Engine::get_ast_as_json, 0))?; Ok(()) diff --git a/bindings/ruby/test/test_regorus.rb b/bindings/ruby/test/test_regorus.rb index 5f37baa..c06c7cc 100644 --- a/bindings/ruby/test/test_regorus.rb +++ b/bindings/ruby/test/test_regorus.rb @@ -183,6 +183,11 @@ class TestRegorus < Minitest::Test assert_equal [":1: Hello"], @engine.take_prints end + def test_set_policy_length_config + @engine.set_policy_length_config({ max_col: 2000, max_file_bytes: 1048576, max_lines: 20000 }) + @engine.clear_policy_length_config + end + def alice_results { result: [ diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 441e902..c286431 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -941,6 +941,7 @@ dependencies = [ "getrandom 0.3.4", "regorus", "serde", + "serde-wasm-bindgen", "serde_json", "uuid", "wasm-bindgen", @@ -990,6 +991,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 7d0f029..42dd89a 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -42,6 +42,7 @@ regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" wasm-bindgen = "0.2.100" +serde-wasm-bindgen = "0.6" # Specify uuid as a mandatory dependency so as to enable `js` feature which is now required # when targeting wasm32-unknown-unknown. uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng", "js"]} diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 25f2b8d..6807535 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -3,6 +3,7 @@ #![allow(non_snake_case)] +use core::num::{NonZeroU32, NonZeroUsize}; use regorus::languages::rego::compiler::Compiler; use regorus::rvm::program::{ generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, @@ -26,6 +27,14 @@ struct ModuleSpec { content: String, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PolicyLengthSpec { + max_col: u32, + max_file_bytes: usize, + max_lines: usize, +} + #[wasm_bindgen] pub struct Program { program: Arc, @@ -183,6 +192,29 @@ impl Engine { self.engine.set_gather_prints(b) } + /// Set the policy length limits used when loading policies. + /// + /// Accepts a JS object: `{ maxCol, maxFileBytes, maxLines }`. + pub fn setPolicyLengthConfig(&mut self, config: JsValue) -> Result<(), JsValue> { + let spec: PolicyLengthSpec = + serde_wasm_bindgen::from_value(config).map_err(error_to_jsvalue)?; + self.engine + .set_policy_length_config(regorus::PolicyLengthConfig { + max_col: NonZeroU32::new(spec.max_col) + .ok_or_else(|| JsValue::from_str("maxCol must be non-zero"))?, + max_file_bytes: NonZeroUsize::new(spec.max_file_bytes) + .ok_or_else(|| JsValue::from_str("maxFileBytes must be non-zero"))?, + max_lines: NonZeroUsize::new(spec.max_lines) + .ok_or_else(|| JsValue::from_str("maxLines must be non-zero"))?, + }); + Ok(()) + } + + /// Clear the policy length configuration, reverting to defaults. + pub fn clearPolicyLengthConfig(&mut self) { + self.engine.clear_policy_length_config(); + } + /// Take the gathered output of print statements. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints diff --git a/bindings/wasm/test.js b/bindings/wasm/test.js index 3995c6f..bbd46b3 100644 --- a/bindings/wasm/test.js +++ b/bindings/wasm/test.js @@ -9,6 +9,9 @@ var engine = new regorus.Engine(); // Enable code coverage engine.setEnableCoverage(true); +// Raise the default col limit to 2000 +engine.setPolicyLengthConfig({ maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000 }); + // Add Rego policy. var pkg = engine.addPolicy( // Associate this file name with policy diff --git a/src/engine.rs b/src/engine.rs index 7ea0682..bb0a70b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -9,6 +9,7 @@ use crate::lexer::*; use crate::parser::*; use crate::scheduler::*; use crate::utils::gather_functions; +use crate::utils::limits::PolicyLengthConfig; use crate::utils::limits::{self, fallback_execution_timer_config, ExecutionTimerConfig}; use crate::value::*; use crate::*; @@ -26,6 +27,7 @@ pub struct Engine { prepared: bool, rego_v1: bool, execution_timer_config: Option, + policy_length_config: PolicyLengthConfig, } #[cfg(feature = "azure_policy")] @@ -83,6 +85,7 @@ impl Engine { prepared: false, rego_v1: true, execution_timer_config: None, + policy_length_config: PolicyLengthConfig::default(), }; engine.apply_effective_execution_timer_config(); engine @@ -144,6 +147,31 @@ impl Engine { self.interpreter.set_execution_timer_config(Some(config)); } + /// Set the policy length limits used when loading policies. + /// + /// Controls maximum file size, line count, and column width for policy files. + /// Engines start with the default limits defined by [`PolicyLengthConfig::default`]. + /// + /// # Examples + /// + /// ``` + /// use core::num::{NonZeroU32, NonZeroUsize}; + /// use regorus::utils::limits::PolicyLengthConfig; + /// use regorus::Engine; + /// + /// let mut engine = Engine::new(); + /// let config = PolicyLengthConfig { + /// max_col: NonZeroU32::new(2048).unwrap(), + /// max_file_bytes: NonZeroUsize::new(2_097_152).unwrap(), + /// max_lines: NonZeroUsize::new(40_000).unwrap(), + /// }; + /// + /// engine.set_policy_length_config(config); + /// ``` + pub const fn set_policy_length_config(&mut self, config: PolicyLengthConfig) { + self.policy_length_config = config; + } + /// Clear the engine-specific execution timer configuration, falling back to the global value. /// /// # Examples @@ -171,6 +199,20 @@ impl Engine { self.apply_effective_execution_timer_config(); } + /// Clear the policy length configuration, reverting to the defaults. + /// + /// # Examples + /// + /// ``` + /// use regorus::Engine; + /// + /// let mut engine = Engine::new(); + /// engine.clear_policy_length_config(); + /// ``` + pub fn clear_policy_length_config(&mut self) { + self.policy_length_config = PolicyLengthConfig::default(); + } + /// Add a policy. /// /// The policy file will be parsed and converted to AST representation. @@ -198,7 +240,12 @@ impl Engine { /// ``` /// pub fn add_policy(&mut self, path: String, rego: String) -> Result { - let source = Source::from_contents(path, rego)?; + let source = Source::from_contents_with_limits( + path, + rego, + self.policy_length_config.max_file_bytes, + self.policy_length_config.max_lines, + )?; let mut parser = self.make_parser(&source)?; let module = Ref::new(parser.parse()?); limits::enforce_memory_limit().map_err(|err| anyhow!(err))?; @@ -232,7 +279,11 @@ impl Engine { #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn add_policy_from_file>(&mut self, path: P) -> Result { - let source = Source::from_file(path)?; + let source = Source::from_file_with_limits( + path, + self.policy_length_config.max_file_bytes, + self.policy_length_config.max_lines, + )?; let mut parser = self.make_parser(&source)?; let module = Ref::new(parser.parse()?); limits::enforce_memory_limit().map_err(|err| anyhow!(err))?; @@ -918,15 +969,24 @@ impl Engine { fn make_query(&mut self, query: String) -> Result<(NodeRef, NodeRef, Schedule)> { let mut query_module = { - let source = Source::from_contents( + let source = Source::from_contents_with_limits( "".to_owned(), "package __internal_query_module".to_owned(), + self.policy_length_config.max_file_bytes, + self.policy_length_config.max_lines, )?; - Parser::new(&source)?.parse()? + let mut parser = Parser::new(&source)?; + parser.set_max_col(self.policy_length_config.max_col); + parser.parse()? }; // Parse the query. - let query_source = Source::from_contents("".to_string(), query)?; + let query_source = Source::from_contents_with_limits( + "".to_string(), + query, + self.policy_length_config.max_file_bytes, + self.policy_length_config.max_lines, + )?; let mut parser = self.make_parser(&query_source)?; let query_node = parser.parse_user_query()?; query_module.num_expressions = parser.num_expressions(); @@ -1506,6 +1566,7 @@ impl Engine { fn make_parser<'a>(&self, source: &'a Source) -> Result> { let mut parser = Parser::new(source)?; + parser.set_max_col(self.policy_length_config.max_col); if self.rego_v1 { parser.enable_rego_v1()?; } @@ -1524,6 +1585,7 @@ impl Engine { rego_v1: true, // Value doesn't matter since this is used only for policy parsing prepared: true, execution_timer_config: None, + policy_length_config: PolicyLengthConfig::default(), // Compiled policies are already parsed, so these length limits are not used }; engine.apply_effective_execution_timer_config(); engine diff --git a/src/lexer.rs b/src/lexer.rs index 549a337..34c6765 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,18 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::utils::limits::{DEFAULT_MAX_COL, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES}; // SAFETY: Arithmetic operations in this module are safe by design: -// 1. MAX_COL=1024 prevents column counter overflow (enforced by advance_col) -// 2. File size is capped by MAX_FILE_BYTES at load time -// 3. Total line count is capped by MAX_LINES at load time +// 1. Column width is bounded by a configurable max_col limit (default DEFAULT_MAX_COL=1024, +// overridable via Engine::set_policy_length_config) and enforced by advance_col +// 2. File size is bounded by a configurable limit (default DEFAULT_MAX_FILE_BYTES) at load time +// 3. Total line count is bounded by a configurable limit (default DEFAULT_MAX_LINES) at load time // 4. State-modifying operations (advance_col/advance_line) use checked arithmetic // 5. Remaining arithmetic is for bounded calculations (spans, error reporting) -// where operands are constrained by MAX_COL and file size/line limits +// where operands are constrained by the column width and file size/line limits // 6. Defensive saturating_sub used for subtractions that could theoretically underflow use crate::*; use core::cmp; use core::fmt::{self, Debug, Formatter}; use core::iter::Peekable; +use core::num::{NonZeroU32, NonZeroUsize}; use core::ops::Range; use core::str::CharIndices; @@ -25,14 +28,6 @@ fn check_memory_limit() -> Result<()> { crate::utils::limits::check_memory_limit_if_needed().map_err(|err| anyhow!(err)) } -// Maximum column width to prevent overflow and catch pathological input. -// Lines exceeding this are likely minified/generated code or attack attempts. -const MAX_COL: u32 = 1024; -// Maximum allowed policy file size in bytes (1 MiB) to reject pathological inputs early. -const MAX_FILE_BYTES: usize = 1_048_576; -// Maximum allowed number of lines to avoid pathological or minified inputs. -const MAX_LINES: usize = 20_000; - #[inline] fn usize_to_u32(value: usize) -> Result { u32::try_from(value).map_err(|_| anyhow!("value exceeds u32::MAX")) @@ -172,8 +167,17 @@ impl cmp::Ord for SourceStr { impl Source { pub fn from_contents(file: String, contents: String) -> Result { - if contents.len() > MAX_FILE_BYTES { - bail!("{file} exceeds maximum allowed policy file size {MAX_FILE_BYTES} bytes"); + Self::from_contents_with_limits(file, contents, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES) + } + + pub fn from_contents_with_limits( + file: String, + contents: String, + max_file_bytes: NonZeroUsize, + max_lines: NonZeroUsize, + ) -> Result { + if contents.len() > max_file_bytes.get() { + bail!("{file} exceeds maximum allowed policy file size {max_file_bytes} bytes",); } let mut lines = vec![]; let mut prev_ch = ' '; @@ -186,8 +190,8 @@ impl Source { '\r' => prev_pos, _ => i_u32, }; - if lines.len() >= MAX_LINES { - bail!("{file} exceeds maximum allowed line count {MAX_LINES}"); + if lines.len() >= max_lines.get() { + bail!("{file} exceeds maximum allowed line count {max_lines}",); } lines.push((start, end)); // Enforce the current global memory cap after recording each line span. @@ -200,8 +204,8 @@ impl Source { let start_usize = usize::try_from(start).unwrap_or(usize::MAX); if start_usize < contents.len() { - if lines.len() >= MAX_LINES { - bail!("{file} exceeds maximum allowed line count {MAX_LINES}"); + if lines.len() >= max_lines.get() { + bail!("{file} exceeds maximum allowed line count {max_lines}",); } lines.push((start, usize_to_u32(contents.len())?)); // Enforce the global limit after appending the final line span. @@ -212,8 +216,8 @@ impl Source { check_memory_limit()?; } else { let s = usize_to_u32(contents.len().saturating_sub(1))?; - if lines.len() >= MAX_LINES { - bail!("{file} exceeds maximum allowed line count {MAX_LINES}"); + if lines.len() >= max_lines.get() { + bail!("{file} exceeds maximum allowed line count {max_lines}",); } lines.push((s, s)); // Enforce the global limit after storing the trailing span. @@ -230,12 +234,26 @@ impl Source { #[cfg(feature = "std")] pub fn from_file>(path: P) -> Result { + Self::from_file_with_limits(path, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES) + } + + #[cfg(feature = "std")] + pub fn from_file_with_limits>( + path: P, + max_file_bytes: NonZeroUsize, + max_lines: NonZeroUsize, + ) -> Result { let contents = match std::fs::read_to_string(&path) { Ok(c) => c, Err(e) => bail!("Failed to read {}. {e}", path.as_ref().display()), }; // TODO: retain path instead of converting to string - Self::from_contents(path.as_ref().to_string_lossy().to_string(), contents) + Self::from_contents_with_limits( + path.as_ref().to_string_lossy().to_string(), + contents, + max_file_bytes, + max_lines, + ) } pub fn file(&self) -> &String { @@ -370,6 +388,7 @@ pub struct Lexer<'source> { iter: Peekable>, line: u32, col: u32, + max_col: NonZeroU32, unknown_char_is_symbol: bool, allow_slash_star_escape: bool, comment_starts_with_double_slash: bool, @@ -393,6 +412,7 @@ impl<'source> Lexer<'source> { iter: source.contents().char_indices().peekable(), line: 1, col: 1, + max_col: DEFAULT_MAX_COL, unknown_char_is_symbol: false, allow_slash_star_escape: false, comment_starts_with_double_slash: false, @@ -420,6 +440,10 @@ impl<'source> Lexer<'source> { self.double_colon_token = b; } + pub const fn set_max_col(&mut self, max_col: NonZeroU32) { + self.max_col = max_col; + } + #[cfg(feature = "azure-rbac")] pub const fn set_enable_rbac_tokens(&mut self, b: bool) { self.enable_rbac_tokens = b; @@ -439,15 +463,16 @@ impl<'source> Lexer<'source> { #[inline] fn advance_col(&mut self, delta: u32) -> Result<()> { + let max_col = self.max_col.get(); let new_col = self .col .checked_add(delta) - .filter(|&c| c <= MAX_COL) + .filter(|&c| c <= max_col) .ok_or_else(|| { self.source.error( self.line, self.col, - &format!("line exceeds maximum column width of {MAX_COL}"), + &format!("line exceeds maximum column width of {max_col}"), ) })?; self.col = new_col; diff --git a/src/lib.rs b/src/lib.rs index 4fb9197..2c45b60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,6 +167,7 @@ pub use engine::Engine; pub use lexer::Source; pub use policy_info::PolicyInfo; pub use utils::limits::LimitError; +pub use utils::limits::PolicyLengthConfig; #[cfg(all(feature = "allocator-memory-limits", not(miri)))] pub use utils::limits::{ check_global_memory_limit, enforce_memory_limit, flush_thread_memory_counters, diff --git a/src/parser.rs b/src/parser.rs index 5c2dd9a..9b0532f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -21,6 +21,7 @@ use crate::value::*; use crate::*; use alloc::collections::BTreeMap; +use core::num::NonZeroU32; use core::str::FromStr; use anyhow::{anyhow, bail, Result}; @@ -198,6 +199,10 @@ impl<'source> Parser<'source> { } } + pub fn set_max_col(&mut self, max_col: NonZeroU32) { + self.lexer.set_max_col(max_col); + } + pub fn get_path_ref_components_into(refr: &Ref, comps: &mut Vec) -> Result<()> { match refr.as_ref() { Expr::RefDot { refr, field, .. } => { diff --git a/src/utils/limits/length.rs b/src/utils/limits/length.rs new file mode 100644 index 0000000..ff36953 --- /dev/null +++ b/src/utils/limits/length.rs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::num::{NonZeroU32, NonZeroUsize}; + +/// Policy source length limits enforced when loading policy files. +/// +/// These limits reject pathological or generated inputs early, before parsing begins. +/// Use [`Default::default`] for the built-in thresholds. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PolicyLengthConfig { + /// Maximum column width per line (default: 1024). + pub max_col: NonZeroU32, + /// Maximum policy file size in bytes (default: 1 MiB). + pub max_file_bytes: NonZeroUsize, + /// Maximum number of lines per policy file (default: 20 000). + pub max_lines: NonZeroUsize, +} + +// Maximum column width to prevent overflow and catch pathological input. +// Lines exceeding this are likely minified/generated code or attack attempts. +pub const DEFAULT_MAX_COL: NonZeroU32 = NonZeroU32::new(1024).unwrap(); +// Maximum allowed policy file size in bytes (1 MiB) to reject pathological inputs early. +pub const DEFAULT_MAX_FILE_BYTES: NonZeroUsize = NonZeroUsize::new(1_048_576).unwrap(); +// Maximum allowed number of lines to avoid pathological or minified inputs. +pub const DEFAULT_MAX_LINES: NonZeroUsize = NonZeroUsize::new(20_000).unwrap(); + +impl Default for PolicyLengthConfig { + fn default() -> Self { + Self { + max_col: DEFAULT_MAX_COL, + max_file_bytes: DEFAULT_MAX_FILE_BYTES, + max_lines: DEFAULT_MAX_LINES, + } + } +} diff --git a/src/utils/limits/mod.rs b/src/utils/limits/mod.rs index 38b2981..69d9c1f 100644 --- a/src/utils/limits/mod.rs +++ b/src/utils/limits/mod.rs @@ -6,6 +6,7 @@ #![allow(dead_code)] mod error; +mod length; #[cfg(all(feature = "allocator-memory-limits", not(miri)))] mod memory; mod time; @@ -27,6 +28,9 @@ pub use time::{ ExecutionTimer, ExecutionTimerConfig, TimeSource, }; +pub use length::PolicyLengthConfig; +pub(crate) use length::{DEFAULT_MAX_COL, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES}; + #[cfg(test)] pub use time::acquire_limits_test_lock; diff --git a/tests/lexer/mod.rs b/tests/lexer/mod.rs index d21c946..66205d4 100644 --- a/tests/lexer/mod.rs +++ b/tests/lexer/mod.rs @@ -270,3 +270,90 @@ fn file_more_than_64_kb_size() -> Result<()> { assert_eq!(count, 8789); Ok(()) } + +#[test] +fn default_limits_reject_oversized_file() { + let big = "x".repeat(1_048_577); + let err = Source::from_contents("big.rego".into(), big).unwrap_err(); + assert!(err + .to_string() + .contains("exceeds maximum allowed policy file size")); +} + +#[test] +fn custom_limits_accept_larger_file() { + use core::num::NonZeroUsize; + + let big = "x".repeat(1_048_577); + assert!(Source::from_contents_with_limits( + "big.rego".into(), + big, + NonZeroUsize::new(2_097_152).unwrap(), + NonZeroUsize::new(1).unwrap() + ) + .is_ok()); +} + +#[test] +fn custom_limits_reject_line_count() { + use core::num::NonZeroUsize; + + let err = Source::from_contents_with_limits( + "lines.rego".into(), + "x\n".repeat(6), + NonZeroUsize::new(100).unwrap(), + NonZeroUsize::new(5).unwrap(), + ) + .unwrap_err(); + assert!(err + .to_string() + .contains("exceeds maximum allowed line count")); +} + +#[test] +fn engine_policy_length_config_flows_through() -> Result<()> { + use core::num::NonZeroUsize; + use regorus::{Engine, PolicyLengthConfig}; + + let mut engine = Engine::new(); + engine.set_policy_length_config(PolicyLengthConfig { + max_file_bytes: NonZeroUsize::new(10).unwrap(), + ..Default::default() + }); + + let err = engine + .add_policy("test.rego".into(), "package test".into()) + .unwrap_err(); + assert!(err + .to_string() + .contains("exceeds maximum allowed policy file size")); + + engine.clear_policy_length_config(); + engine.add_policy("test.rego".into(), "package test".into())?; + Ok(()) +} + +#[test] +fn custom_max_col_allows_wide_line() -> Result<()> { + use core::num::NonZeroU32; + use regorus::{Engine, PolicyLengthConfig}; + + // A line wider than the default 1024 columns. + let wide = format!("package test\na := \"{}\"", "x".repeat(2000)); + + let mut engine = Engine::new(); + + // Should fail with default limits. + let err = engine + .add_policy("wide.rego".into(), wide.clone()) + .unwrap_err(); + assert!(err.to_string().contains("maximum column width")); + + // Should succeed with a raised max_col. + engine.set_policy_length_config(PolicyLengthConfig { + max_col: NonZeroU32::new(4096).unwrap(), + ..Default::default() + }); + engine.add_policy("wide.rego".into(), wide)?; + Ok(()) +}