Skip to content

Commit e5da647

Browse files
Maksym MishchenkoCopilot
andcommitted
fix(rvm): restore execute-only memory budgets
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3ac1c1a commit e5da647

15 files changed

Lines changed: 265 additions & 1032 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010

1111
- *(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))
12-
- *(rvm,ffi,csharp)* add scoped evaluation-memory budgeting via `RegoVM::with_evaluation_memory_budget` and one-call data execution helpers for charging evaluation-specific data with execution ([#792](https://github.com/microsoft/regorus/pull/792))
1312

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

benches/rvm_benchmark.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ use regorus::{Engine, Rc, Value};
6161
// hot path (memory_check, execution_timer_tick, instruction-limit compare).
6262
// ---------------------------------------------------------------------------
6363

64-
#[cfg(feature = "allocator-memory-limits")]
64+
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
6565
const MEMORY_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
6666
const TIME_LIMIT: Duration = Duration::from_secs(30);
6767
const TIMER_CHECK_INTERVAL: NonZeroU32 = NonZeroU32::new(16).unwrap();
@@ -375,16 +375,19 @@ fn compile_all_programs() -> Vec<BenchmarkProgram> {
375375

376376
/// Apply the limits selected for one benchmark configuration.
377377
fn configure_limits(vm: &mut RegoVM, config: EvalConfig) {
378+
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
379+
let _ = config.memory_budget;
380+
378381
if config.limits {
379-
#[cfg(feature = "allocator-memory-limits")]
382+
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
380383
regorus::set_global_memory_limit(Some(MEMORY_LIMIT_BYTES));
381384
vm.set_execution_timer_config(Some(ExecutionTimerConfig {
382385
limit: TIME_LIMIT,
383386
check_interval: TIMER_CHECK_INTERVAL,
384387
}));
385388
vm.set_max_instructions(INSTRUCTION_LIMIT);
386389
} else {
387-
#[cfg(feature = "allocator-memory-limits")]
390+
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
388391
regorus::set_global_memory_limit(None);
389392
vm.set_execution_timer_config(None);
390393
vm.set_max_instructions(usize::MAX);

bindings/csharp/API.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,10 @@ public sealed class Rvm : IDisposable
9898
{
9999
public void SetMemoryBudgetConfig(MemoryBudgetConfig config);
100100
public void ClearMemoryBudgetConfig();
101-
102-
public string? ExecuteWithDataJson(string dataJson);
103-
public string? ExecuteEntryPointWithDataJson(string entryPoint, string dataJson);
104-
public string? ExecuteEntryPointWithDataJson(ulong index, string dataJson);
105101
}
106102
```
107103

108-
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 are excluded; use this path for static or preloaded data.
104+
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.
109105

110106
```csharp
111107
using var vm = new Rvm();
@@ -121,11 +117,9 @@ catch (RegorusMemoryBudgetExceededException)
121117
}
122118
```
123119

124-
Use `ExecuteWithDataJson`, `ExecuteEntryPointWithDataJson(string, string)`, or `ExecuteEntryPointWithDataJson(ulong, string)` when evaluation-specific data should be charged with execution. These methods make one native call that includes native JSON parsing/storage for `dataJson`, execution, native JSON serialization, and native C-string allocation. Managed UTF-8 decoding and the managed C# `string` allocation after the native call returns are not charged.
125-
126-
If scoped data replacement fails, the VM keeps its previous data. The previous and provisional native data can coexist transiently, so both count toward the peak live bytes observed by the budget.
120+
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.
127121

128-
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. Public multi-call begin/end scopes are intentionally absent because allocator counters are thread-local and abandoned or cross-thread scopes would be unsafe.
122+
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.
129123

130124
## Core Classes
131125

bindings/csharp/README.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Console.WriteLine($"allow: {result}");
107107

108108
### Per-execution memory budget
109109

110-
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 are not charged.
110+
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.
111111

112112
```csharp
113113
using var vm = new Rvm();
@@ -126,17 +126,9 @@ catch (RegorusMemoryBudgetExceededException ex)
126126
}
127127
```
128128

129-
Use the one-call `Execute*WithDataJson` methods when evaluation-specific data should be charged in the same native budget scope as execution:
129+
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.
130130

131-
```csharp
132-
var result = vm.ExecuteWithDataJson(Data);
133-
var named = vm.ExecuteEntryPointWithDataJson("data.demo.allow", Data);
134-
var indexed = vm.ExecuteEntryPointWithDataJson(0UL, Data);
135-
```
136-
137-
Those methods include native JSON parsing/storage for `dataJson`, execution, native result JSON serialization, and native C-string allocation. Managed UTF-8 decoding and C# `string` allocation after the native call returns are not charged. If scoped data replacement fails, the previous VM data is preserved, but the previous and provisional data may coexist transiently and count toward peak live bytes.
138-
139-
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. The process-wide limit exposed by `MemoryLimits` remains a separate safeguard.
131+
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.
140132

141133
## Azure RBAC Condition Evaluation
142134

bindings/csharp/Regorus.Tests/RvmMemoryBudgetTests.cs

Lines changed: 0 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,6 @@ package limits.memory
3030

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

33-
private const string DataResultPolicy = """
34-
package limits.memory
35-
36-
main := data.value
37-
named := data.value
38-
""";
39-
40-
private const string MainEntryPoint = "data.limits.memory.main";
41-
42-
private const string NamedEntryPoint = "data.limits.memory.named";
43-
4433
[TestMethod]
4534
public void Memory_budget_must_be_non_zero()
4635
{
@@ -96,122 +85,6 @@ public void Serialization_budget_failure_leaves_error_state()
9685
StringAssert.Contains(state, "Error { error: MemoryBudgetExceeded");
9786
}
9887

99-
[TestMethod]
100-
public void Execute_with_data_json_runs_main_entry_point()
101-
{
102-
using var program = CreateDataResultProgram();
103-
using var vm = CreateRvm(program);
104-
105-
var result = vm.ExecuteWithDataJson(CreateValueData("one-call"));
106-
107-
Assert.AreEqual("\"one-call\"", result);
108-
}
109-
110-
[TestMethod]
111-
public void Execute_entry_point_with_data_json_runs_named_and_indexed_entry_points()
112-
{
113-
using var program = CreateDataResultProgram();
114-
using var vm = CreateRvm(program);
115-
116-
Assert.AreEqual("\"named\"", vm.ExecuteEntryPointWithDataJson(NamedEntryPoint, CreateValueData("named")));
117-
Assert.AreEqual("\"indexed\"", vm.ExecuteEntryPointWithDataJson(0, CreateValueData("indexed")));
118-
}
119-
120-
[TestMethod]
121-
public void Execute_with_data_json_exceeding_data_budget_throws_typed_exception()
122-
{
123-
using var program = CreateDataResultProgram();
124-
using var vm = CreateRvm(program);
125-
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));
126-
127-
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(
128-
() => vm.ExecuteWithDataJson(CreateOversizedValueData()));
129-
}
130-
131-
[TestMethod]
132-
public void Execute_with_data_json_failed_replacement_preserves_previous_vm_data()
133-
{
134-
using var program = CreateDataResultProgram();
135-
using var vm = CreateRvm(program);
136-
vm.SetDataJson(CreateValueData("previous"));
137-
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));
138-
139-
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(
140-
() => vm.ExecuteWithDataJson(CreateOversizedValueData()));
141-
142-
var state = vm.GetExecutionState();
143-
Assert.IsNotNull(state);
144-
StringAssert.Contains(state, "Error { error: MemoryBudgetExceeded");
145-
146-
vm.ClearMemoryBudgetConfig();
147-
148-
Assert.AreEqual("\"previous\"", vm.ExecuteEntryPoint(MainEntryPoint));
149-
}
150-
151-
[TestMethod]
152-
public void Execute_with_malformed_data_json_leaves_error_state_and_preserves_previous_vm_data()
153-
{
154-
using var program = CreateDataResultProgram();
155-
using var vm = CreateRvm(program);
156-
vm.SetDataJson(CreateValueData("previous"));
157-
158-
Assert.ThrowsException<InvalidOperationException>(() => vm.ExecuteWithDataJson("{"));
159-
160-
var state = vm.GetExecutionState();
161-
Assert.IsNotNull(state);
162-
StringAssert.Contains(state, "Error { error:");
163-
164-
Assert.AreEqual("\"previous\"", vm.ExecuteEntryPoint(MainEntryPoint));
165-
}
166-
167-
[TestMethod]
168-
public void Execute_with_data_json_reuses_vm_with_a_fresh_budget_after_success_and_failure()
169-
{
170-
using var program = CreateDataResultProgram();
171-
using var vm = CreateRvm(program);
172-
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));
173-
174-
Assert.AreEqual("\"first\"", vm.ExecuteWithDataJson(CreateValueData("first")));
175-
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(
176-
() => vm.ExecuteWithDataJson(CreateOversizedValueData()));
177-
Assert.AreEqual("\"second\"", vm.ExecuteWithDataJson(CreateValueData("second")));
178-
}
179-
180-
[TestMethod]
181-
public void Execute_with_data_json_uses_unlimited_default_and_cleared_budget()
182-
{
183-
using var program = CreateDataResultProgram();
184-
using var vm = CreateRvm(program);
185-
186-
Assert.AreEqual("\"default\"", vm.ExecuteWithDataJson(CreateValueData("default")));
187-
188-
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(TightMemoryBudgetBytes));
189-
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(
190-
() => vm.ExecuteWithDataJson(CreateOversizedValueData()));
191-
192-
vm.ClearMemoryBudgetConfig();
193-
194-
var result = vm.ExecuteWithDataJson(CreateOversizedValueData());
195-
Assert.IsFalse(string.IsNullOrWhiteSpace(result));
196-
}
197-
198-
[TestMethod]
199-
public void Execute_with_data_json_counts_native_result_serialization_but_not_managed_string_copy()
200-
{
201-
using var program = CreateLargeResultProgram();
202-
using var vm = CreateRvm(program);
203-
vm.SetInputJson(CreateLargeResultInput());
204-
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(512 * 1024));
205-
206-
Assert.ThrowsException<RegorusMemoryBudgetExceededException>(() => vm.ExecuteWithDataJson("{}"));
207-
208-
vm.ClearMemoryBudgetConfig();
209-
210-
var result = vm.ExecuteWithDataJson("{}");
211-
Assert.IsNotNull(result);
212-
Assert.AreEqual((2 * 1024 * 1024) + 2, result!.Length);
213-
}
214-
21588
[TestMethod]
21689
public void Suspendable_execution_rejects_memory_budget()
21790
{
@@ -222,38 +95,6 @@ public void Suspendable_execution_rejects_memory_budget()
22295
Assert.ThrowsException<RegorusMemoryBudgetUnsupportedException>(() => vm.Execute());
22396
}
22497

225-
private static Program CreateDataResultProgram()
226-
{
227-
var modules = new[] { new PolicyModule("data_result.rego", DataResultPolicy) };
228-
return Program.CompileFromModules("{}", modules, new[] { MainEntryPoint, NamedEntryPoint });
229-
}
230-
231-
private static Program CreateLargeResultProgram()
232-
{
233-
const string policy = """
234-
package limits.memory
235-
236-
large_string := input.large_string
237-
""";
238-
var modules = new[] { new PolicyModule("large_result.rego", policy) };
239-
return Program.CompileFromModules("{}", modules, new[] { "data.limits.memory.large_string" });
240-
}
241-
242-
private static string CreateLargeResultInput()
243-
{
244-
return JsonSerializer.Serialize(new { large_string = new string('x', 2 * 1024 * 1024) });
245-
}
246-
247-
private static string CreateValueData(string value)
248-
{
249-
return JsonSerializer.Serialize(new { value });
250-
}
251-
252-
private static string CreateOversizedValueData()
253-
{
254-
return CreateValueData(new string('x', 2 * 1024 * 1024));
255-
}
256-
25798
private static Program CreateProgram()
25899
{
259100
var modules = new[] { new PolicyModule("memory_budget.rego", Policy) };

bindings/csharp/Regorus/NativeMethods.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -204,24 +204,6 @@ internal static unsafe partial class API
204204
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_index", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
205205
internal static extern RegorusResult regorus_rvm_execute_entry_point_by_index(RegorusRvm* vm, UIntPtr index);
206206

207-
/// <summary>
208-
/// Set the data document and execute the program in one native call.
209-
/// </summary>
210-
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_with_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
211-
internal static extern RegorusResult regorus_rvm_execute_with_data(RegorusRvm* vm, byte* data_json);
212-
213-
/// <summary>
214-
/// Set the data document and execute a named entry point in one native call.
215-
/// </summary>
216-
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_name_with_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
217-
internal static extern RegorusResult regorus_rvm_execute_entry_point_by_name_with_data(RegorusRvm* vm, byte* entry_point, byte* data_json);
218-
219-
/// <summary>
220-
/// Set the data document and execute an entry point by index in one native call.
221-
/// </summary>
222-
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_index_with_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
223-
internal static extern RegorusResult regorus_rvm_execute_entry_point_by_index_with_data(RegorusRvm* vm, UIntPtr index, byte* data_json);
224-
225207
/// <summary>
226208
/// Resume execution.
227209
/// </summary>

bindings/csharp/Regorus/Rvm.cs

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -212,57 +212,6 @@ public void ClearMemoryBudgetConfig()
212212
});
213213
}
214214

215-
/// <summary>
216-
/// Set evaluation-specific data JSON and execute the program in one native call.
217-
/// </summary>
218-
public string? ExecuteWithDataJson(string dataJson)
219-
{
220-
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
221-
{
222-
return UseHandle(vmPtr =>
223-
{
224-
return CheckAndDropResult(API.regorus_rvm_execute_with_data((RegorusRvm*)vmPtr, (byte*)dataPtr));
225-
});
226-
});
227-
}
228-
229-
/// <summary>
230-
/// Set evaluation-specific data JSON and execute a named entry point in one native call.
231-
/// </summary>
232-
public string? ExecuteEntryPointWithDataJson(string entryPoint, string dataJson)
233-
{
234-
return Utf8Marshaller.WithUtf8(entryPoint, entryPtr =>
235-
{
236-
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
237-
{
238-
return UseHandle(vmPtr =>
239-
{
240-
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_name_with_data(
241-
(RegorusRvm*)vmPtr,
242-
(byte*)entryPtr,
243-
(byte*)dataPtr));
244-
});
245-
});
246-
});
247-
}
248-
249-
/// <summary>
250-
/// Set evaluation-specific data JSON and execute an entry point by index in one native call.
251-
/// </summary>
252-
public string? ExecuteEntryPointWithDataJson(ulong index, string dataJson)
253-
{
254-
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
255-
{
256-
return UseHandle(vmPtr =>
257-
{
258-
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index_with_data(
259-
(RegorusRvm*)vmPtr,
260-
(UIntPtr)index,
261-
(byte*)dataPtr));
262-
});
263-
});
264-
}
265-
266215
/// <summary>
267216
/// Resume execution with an optional value.
268217
/// </summary>

bindings/ffi/CHANGELOG.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11-
- Add `regorus_rvm_set_memory_budget_config`, `RegorusMemoryBudgetConfig`, and appended statuses for memory-budget exhaustion and unsupported suspendable execution.
12-
- Add one-call RVM data execution APIs: `regorus_rvm_execute_with_data`,
13-
`regorus_rvm_execute_entry_point_by_name_with_data`, and
14-
`regorus_rvm_execute_entry_point_by_index_with_data`. Their memory-budget scope covers data
15-
preparation, execution, and native result production.
11+
- Add execute-only RVM memory-budget configuration through
12+
`regorus_rvm_set_memory_budget_config`, `RegorusMemoryBudgetConfig`, and appended statuses for
13+
exhaustion and unsupported suspendable execution. Native execute result serialization and C-string
14+
allocation are included in the configured budget.
1615

1716
## [0.1.0](https://github.com/microsoft/regorus/releases/tag/regorus-ffi-v0.1.0) - 2024-02-08
1817

0 commit comments

Comments
 (0)