Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- *(rvm)* add opt-in per-execution memory budgets for run-to-completion evaluation, including typed Rust and binding errors ([#792](https://github.com/microsoft/regorus/pull/792))

## [0.11.0](https://github.com/microsoft/regorus/compare/regorus-v0.10.1...regorus-v0.11.0) - 2026-07-21

### Added
Expand Down Expand Up @@ -634,4 +638,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- LICENSE committed
- CODE_OF_CONDUCT.md committed
- Initial commit

41 changes: 32 additions & 9 deletions benches/rvm_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

use std::hint::black_box;
use std::num::NonZeroU32;
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use std::num::NonZeroU64;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
Expand All @@ -50,14 +52,16 @@ use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::Program;
use regorus::rvm::vm::{ExecutionMode, RegoVM};
use regorus::utils::limits::ExecutionTimerConfig;
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use regorus::MemoryBudgetConfig;
use regorus::{Engine, Rc, Value};

// ---------------------------------------------------------------------------
// Limit constants – generous ceilings that still exercise the limit-checking
// hot path (memory_check, execution_timer_tick, instruction-limit compare).
// ---------------------------------------------------------------------------

#[cfg(feature = "allocator-memory-limits")]
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
const MEMORY_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
const TIME_LIMIT: Duration = Duration::from_secs(30);
const TIMER_CHECK_INTERVAL: NonZeroU32 = NonZeroU32::new(16).unwrap();
Expand All @@ -68,28 +72,39 @@ struct EvalConfig {
name: &'static str,
mode: ExecutionMode,
limits: bool,
memory_budget: bool,
}

const EVAL_CONFIGS: [EvalConfig; 4] = [
const EVAL_CONFIGS: [EvalConfig; 5] = [
EvalConfig {
name: "regular_no_limits",
mode: ExecutionMode::RunToCompletion,
limits: false,
memory_budget: false,
},
EvalConfig {
name: "regular_memory_budget",
mode: ExecutionMode::RunToCompletion,
limits: false,
memory_budget: true,
},
EvalConfig {
name: "regular_with_limits",
mode: ExecutionMode::RunToCompletion,
limits: true,
memory_budget: true,
},
EvalConfig {
name: "suspendable_no_limits",
mode: ExecutionMode::Suspendable,
limits: false,
memory_budget: false,
},
EvalConfig {
name: "suspendable_with_limits",
mode: ExecutionMode::Suspendable,
limits: true,
memory_budget: false,
},
];

Expand Down Expand Up @@ -358,22 +373,30 @@ fn compile_all_programs() -> Vec<BenchmarkProgram> {
// Limit helpers
// ---------------------------------------------------------------------------

/// Apply or remove production-style limits based on a boolean flag.
fn configure_limits(vm: &mut RegoVM, limits: bool) {
if limits {
#[cfg(feature = "allocator-memory-limits")]
/// Apply the limits selected for one benchmark configuration.
fn configure_limits(vm: &mut RegoVM, config: EvalConfig) {
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
let _ = config.memory_budget;

if config.limits {
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
regorus::set_global_memory_limit(Some(MEMORY_LIMIT_BYTES));
vm.set_execution_timer_config(Some(ExecutionTimerConfig {
limit: TIME_LIMIT,
check_interval: TIMER_CHECK_INTERVAL,
}));
vm.set_max_instructions(INSTRUCTION_LIMIT);
} else {
#[cfg(feature = "allocator-memory-limits")]
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
regorus::set_global_memory_limit(None);
vm.set_execution_timer_config(None);
vm.set_max_instructions(usize::MAX);
}

#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
vm.set_memory_budget_config(config.memory_budget.then(|| MemoryBudgetConfig {

@anakrish Anand Krishnamoorthi (anakrish) Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: These benchmark imports/calls use #[cfg(feature = "allocator-memory-limits")], but the core MemoryBudgetConfig export and RegoVM::set_memory_budget_config are gated by all(feature = "allocator-memory-limits", not(miri)). A bench build selected under Miri can therefore fail to compile.

Suggested change:

#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use std::num::NonZeroU64;

#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use regorus::MemoryBudgetConfig;

#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
vm.set_memory_budget_config(config.memory_budget.then(|| MemoryBudgetConfig {
    limit: NonZeroU64::new(MEMORY_LIMIT_BYTES).expect("non-zero memory budget"),
}));

Please use the same predicate for both imports and the configuration block.

limit: NonZeroU64::new(MEMORY_LIMIT_BYTES).expect("non-zero memory budget"),
}));
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -408,7 +431,7 @@ fn bench_cold(c: &mut Criterion) {
vm.set_data(black_box(d.clone())).unwrap();
}
vm.set_input(black_box(input.clone()));
configure_limits(&mut vm, config.limits);
configure_limits(&mut vm, config);
black_box(vm.execute().unwrap())
})
});
Expand Down Expand Up @@ -445,7 +468,7 @@ fn bench_hot(c: &mut Criterion) {
if let Some(ref d) = data {
vm.set_data(d.clone()).unwrap();
}
configure_limits(&mut vm, config.limits);
configure_limits(&mut vm, config);

// Warm up: fill register window pools, caches, etc.
vm.set_input(inputs[0].clone());
Expand Down
39 changes: 39 additions & 0 deletions bindings/csharp/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,45 @@ The Regorus C# bindings provide a modern, thread-safe API for compiling and eval
- **Thread Safety**: All operations are thread-safe without external synchronization
- **Registry Management**: Centralized management of targets and schemas
- **Policy Introspection**: Rich metadata about compiled policies
- **RVM Memory Budgets**: Optional per-execution live-memory limits for run-to-completion evaluation

## RVM Memory Budgets

`Rvm.SetMemoryBudgetConfig` configures a fresh non-zero budget for each run-to-completion execution. `Rvm.ClearMemoryBudgetConfig` restores unlimited execution.

```csharp
public readonly struct MemoryBudgetConfig
{
public MemoryBudgetConfig(ulong limitBytes);
public ulong LimitBytes { get; }
}

public sealed class Rvm : IDisposable
{
public void SetMemoryBudgetConfig(MemoryBudgetConfig config);
public void ClearMemoryBudgetConfig();
}
```

Ordinary `Execute` and `ExecuteEntryPoint` calls use the existing VM data and start a fresh budget for execution. Program compilation, program loading, and prior `SetDataJson`, `SetInputJson`, and `SetContextJson` calls occur before and outside the budget; use this path for static or preloaded data.

```csharp
using var vm = new Rvm();
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(16UL * 1024 * 1024));

try
{
var result = vm.Execute();
}
catch (RegorusMemoryBudgetExceededException)
{
// The execution exceeded its configured budget.
}
```

Native result JSON serialization and C-string allocation are included before an `Execute` or `ExecuteEntryPoint` call returns. Managed UTF-8 decoding and the managed C# `string` allocation after the native call returns are excluded.

Memory budgets require a native library built with allocator memory tracking and are supported only for run-to-completion execution. `RegorusMemoryBudgetExceededException` is thrown when a budget is exceeded. `RegorusMemoryBudgetUnsupportedException` is thrown if a configured budget is used to start or resume suspendable execution. Enforcement is cooperative, so one instruction can overshoot before the next checkpoint. Same-thread baseline ratcheting can make the effective limit stricter after unrelated frees are observed; those frees are never credited back. Failed terminal execution clears retained state, and a reused VM gets a fresh budget. Public multi-call begin/end scopes are intentionally absent because allocator counters are thread-local and abandoned or cross-thread scopes would be unsafe.

## Core Classes

Expand Down
25 changes: 25 additions & 0 deletions bindings/csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ var result = vm.Execute();
Console.WriteLine($"allow: {result}");
```

### Per-execution memory budget

RVM run-to-completion evaluation can use an optional additional live-memory budget. Each ordinary `Execute` or `ExecuteEntryPoint` call starts with a fresh budget for execution; program compilation, program loading, and prior `SetDataJson`, `SetInputJson`, and `SetContextJson` calls occur before and outside that budget.

```csharp
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(Data);
vm.SetInputJson(Input);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(16 * 1024 * 1024));

try
{
var result = vm.Execute();
}
catch (RegorusMemoryBudgetExceededException ex)
{
Console.WriteLine(ex.Message);
}
```

The native execution budget includes result JSON serialization and C-string allocation before the native call returns. Managed UTF-8 decoding and C# `string` allocation after that return are not charged.

The budget is cooperative and may overshoot between VM checks. Same-thread allocation-counter baseline ratcheting can make the effective limit stricter after unrelated frees are observed; those frees are not credited back. Budgets are not supported in suspendable execution mode. `ClearMemoryBudgetConfig` restores the previous unlimited per-execution behavior. Public multi-call begin/end scopes are intentionally absent because allocator counters are thread-local. Failed terminal execution clears retained state, and a reused VM starts a fresh budget. The process-wide limit exposed by `MemoryLimits` remains a separate safeguard.

## Azure RBAC Condition Evaluation

Evaluate Azure RBAC condition expressions directly with a JSON evaluation context:
Expand Down
120 changes: 120 additions & 0 deletions bindings/csharp/Regorus.Tests/RvmMemoryBudgetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Regorus.Tests;

[TestClass]
public sealed class RvmMemoryBudgetTests
{
private const ulong TightMemoryBudgetBytes = 64 * 1024;

private const string Policy = """
package limits.memory
import rego.v1

large_array := json.unmarshal(data.large_json)
""";

private const string EntryPoint = "data.limits.memory.large_array";

private const string PreloadedResultPolicy = """
package limits.memory

large_string := data.large_string
""";

private const string PreloadedResultEntryPoint = "data.limits.memory.large_string";

[TestMethod]
public void Memory_budget_must_be_non_zero()
{
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new MemoryBudgetConfig(0));

using var vm = new Rvm();
Assert.ThrowsException<ArgumentOutOfRangeException>(() => vm.SetMemoryBudgetConfig(default));
}

[TestMethod]
public void Execute_exceeding_memory_budget_throws_typed_exception()
{
using var program = CreateProgram();
using var vm = CreateRvm(program);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));

Assert.ThrowsException<RegorusMemoryBudgetExceededException>(() => vm.ExecuteEntryPoint(EntryPoint));
}

[TestMethod]
public void Clearing_memory_budget_restores_unlimited_execution()
{
using var program = CreateProgram();
using var vm = CreateRvm(program);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(() => vm.ExecuteEntryPoint(EntryPoint));

vm.ClearMemoryBudgetConfig();

var result = vm.ExecuteEntryPoint(EntryPoint);
Assert.IsFalse(string.IsNullOrWhiteSpace(result));
}

[TestMethod]
public void Serialization_budget_failure_leaves_error_state()
{
var data = JsonSerializer.Serialize(new
{
large_string = new string('x', 2 * 1024 * 1024),
});
var modules = new[] { new PolicyModule("memory_budget.rego", PreloadedResultPolicy) };
using var program = Program.CompileFromModules(data, modules, new[] { PreloadedResultEntryPoint });
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(data);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(512 * 1024));

Assert.ThrowsException<RegorusMemoryBudgetExceededException>(
() => vm.ExecuteEntryPoint(PreloadedResultEntryPoint));

var state = vm.GetExecutionState();
Assert.IsNotNull(state);
StringAssert.Contains(state, "Error { error: MemoryBudgetExceeded");
}

[TestMethod]
public void Suspendable_execution_rejects_memory_budget()
{
using var vm = new Rvm();
vm.SetExecutionMode(ExecutionMode.Suspendable);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(1024));

Assert.ThrowsException<RegorusMemoryBudgetUnsupportedException>(() => vm.Execute());
}

private static Program CreateProgram()
{
var modules = new[] { new PolicyModule("memory_budget.rego", Policy) };
return Program.CompileFromModules(CreateData(), modules, new[] { EntryPoint });
}

private static Rvm CreateRvm(Program program)
{
var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(CreateData());
return vm;
}

private static string CreateData()
{
var values = Enumerable.Range(0, 200_000).ToArray();
return JsonSerializer.Serialize(new
{
large_json = JsonSerializer.Serialize(values),
});
}
}
46 changes: 46 additions & 0 deletions bindings/csharp/Regorus/MemoryBudgetConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace Regorus
{
/// <summary>
/// Configures the additional live-memory budget for one RVM execution.
/// </summary>
public readonly struct MemoryBudgetConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="MemoryBudgetConfig"/> struct.
/// </summary>
/// <param name="limitBytes">Maximum additional live bytes allowed during one execution.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="limitBytes"/> is zero.</exception>
public MemoryBudgetConfig(ulong limitBytes)
Comment thread
maksym-mishchenko marked this conversation as resolved.
{
if (limitBytes == 0)
{
throw new ArgumentOutOfRangeException(nameof(limitBytes), "Memory budget must be non-zero.");
}

LimitBytes = limitBytes;
}

/// <summary>
/// Gets the maximum additional live bytes allowed during one execution.
/// </summary>
public ulong LimitBytes { get; }

internal Regorus.Internal.RegorusMemoryBudgetConfig ToNative()
{
if (LimitBytes == 0)
{
throw new ArgumentOutOfRangeException(nameof(LimitBytes), "Memory budget must be non-zero.");
}

return new Regorus.Internal.RegorusMemoryBudgetConfig
{
limit_bytes = LimitBytes,
};
}
}
}
Loading
Loading