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
This commit is contained in:
antmhs
2026-03-13 19:19:57 +02:00
committed by GitHub
parent 50c0215fdb
commit 898643129e
32 changed files with 726 additions and 53 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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 =>

View File

@@ -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);
/// <summary>
/// Set the policy length limits for a specific engine instance.
/// </summary>
[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);
/// <summary>
/// Clear the policy length configuration for a specific engine instance.
/// </summary>
[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;
}
/// <summary>
/// FFI representation of the policy length configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusPolicyLengthConfig
{
public uint max_col;
public UIntPtr max_file_bytes;
public UIntPtr max_lines;
}
/// <summary>
/// Byte buffer returned from FFI.
/// </summary>

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace Regorus
{
/// <summary>
/// Policy source length limits enforced when loading policy files.
/// </summary>
public readonly struct PolicyLengthConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="PolicyLengthConfig"/> struct.
/// </summary>
/// <param name="maxCol">Maximum column width per line. Must be non-zero.</param>
/// <param name="maxFileBytes">Maximum policy file size in bytes. Must be non-zero.</param>
/// <param name="maxLines">Maximum number of lines per policy file. Must be non-zero.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any parameter is zero.</exception>
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;
}
/// <summary>Maximum column width per line (default: 1024).</summary>
public uint MaxCol { get; }
/// <summary>Maximum policy file size in bytes (default: 1 MiB).</summary>
public nuint MaxFileBytes { get; }
/// <summary>Maximum number of lines per policy file (default: 20000).</summary>
public nuint MaxLines { get; }
internal Regorus.Internal.RegorusPolicyLengthConfig ToNative()
{
return new Regorus.Internal.RegorusPolicyLengthConfig
{
max_col = MaxCol,
max_file_bytes = MaxFileBytes,
max_lines = MaxLines,
};
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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<regorus::PolicyLengthConfig> {
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,

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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')

View File

@@ -962,6 +962,7 @@ version = "0.9.1"
dependencies = [
"magnus",
"regorus",
"serde",
"serde_json",
"serde_magnus",
]

View File

@@ -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"

View File

@@ -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<magnus::Value, Error> {
let results = self
fn eval_query(ruby: &Ruby, rb_self: &Self, query: String) -> Result<magnus::Value, Error> {
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<Option<magnus::Value>, Error> {
fn eval_rule(
ruby: &Ruby,
rb_self: &Self,
query: String,
) -> Result<Option<magnus::Value>, 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<String, Error> {
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(())

View File

@@ -183,6 +183,11 @@ class TestRegorus < Minitest::Test
assert_equal ["<query.rego>: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: [

View File

@@ -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"

View File

@@ -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"]}

View File

@@ -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<RvmProgram>,
@@ -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

View File

@@ -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

View File

@@ -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<ExecutionTimerConfig>,
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<String> {
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<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<String> {
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<Module>, NodeRef<Query>, Schedule)> {
let mut query_module = {
let source = Source::from_contents(
let source = Source::from_contents_with_limits(
"<query_module.rego>".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("<query.rego>".to_string(), query)?;
let query_source = Source::from_contents_with_limits(
"<query.rego>".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<Parser<'a>> {
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

View File

@@ -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> {
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<Source> {
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<Source> {
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<P: AsRef<std::path::Path>>(path: P) -> Result<Source> {
Self::from_file_with_limits(path, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES)
}
#[cfg(feature = "std")]
pub fn from_file_with_limits<P: AsRef<std::path::Path>>(
path: P,
max_file_bytes: NonZeroUsize,
max_lines: NonZeroUsize,
) -> Result<Source> {
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<CharIndices<'source>>,
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;

View File

@@ -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,

View File

@@ -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<Expr>, comps: &mut Vec<Span>) -> Result<()> {
match refr.as_ref() {
Expr::RefDot { refr, field, .. } => {

View File

@@ -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,
}
}
}

View File

@@ -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;

View File

@@ -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(())
}