Files
regorus/bindings/ffi/src/rvm.rs
Anand Krishnamoorthi 86b4a279fa fix(ffi): eliminate aliasing UB + add Azure Policy JSON compilation FFI (#727)
* fix(ffi): eliminate aliasing UB via to_shared_ref migration

Add to_shared_ref() helper that creates &T (shared reference) from raw
pointers instead of &mut T. This eliminates undefined behavior caused by
violating Rust's aliasing invariant when C# SafeHandle permits concurrent
FFI calls on the same handle.

With &mut T, the compiler may assume exclusive (noalias) access and
reorder or elide reads/writes — a miscompilation risk when another thread
holds a reference to the same object. Switching to &T removes that
assumption; actual mutation is mediated by the interior RwLock inside
Handle<T>, which is the sole synchronization mechanism.

Migrated sites:
- rvm.rs: 20 non-drop call sites
- engine.rs: 30 non-drop call sites + with_unwind_guard for timer fns
- compiled_policy.rs: 2 call sites
- Fix null-data UB in regorus_program_deserialize_binary

Drop paths retain to_ref() where exclusive access is guaranteed by the
caller contract (preventing use-after-free).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(ffi): add Azure Policy JSON compilation FFI and C# bindings

- AliasRegistry builder pattern: RegorusAliasRegistryBuilder (mutable,
  single-threaded) + RegorusAliasRegistry (immutable, Arc-wrapped)
- Azure Policy JSON compilation: regorus_compile_azure_policy_rule and
  regorus_compile_azure_policy_definition with alias registry support
- regorus_rvm_set_context for host-supplied ambient data
- C# AliasRegistryBuilder and AliasRegistry classes with convenience
  factories (FromJson, FromManifest, Empty)
- C# AzurePolicyCompiler static class for policy rule/definition compilation
- Compile functions take *const RegorusAliasRegistry (read-only via
  to_shared_ref for concurrent compilation safety)
- Fix pre-existing clippy warnings across multiple crates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-22 12:50:16 -05:00

643 lines
21 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{
from_c_str, to_ref, to_regorus_result, to_shared_ref, RegorusBuffer, RegorusResult,
RegorusStatus,
};
use crate::compile::RegorusPolicyModule;
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig;
use crate::lock::{new_handle, try_read, try_write, Handle, ReadGuard, WriteGuard};
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use anyhow::{anyhow, Result};
use core::ffi::{c_char, c_void};
use core::ptr;
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
DeserializationResult, Program,
};
use regorus::rvm::vm::{ExecutionMode, ExecutionState, RegoVM};
use regorus::PolicyModule;
use regorus::Value;
/// Wrapper for `regorus::rvm::Program`.
#[derive(Clone)]
pub struct RegorusProgram {
pub(crate) program: Arc<Program>,
}
/// Wrapper for `regorus::rvm::RegoVM`.
pub struct RegorusRvm {
vm: Handle<RegoVM>,
}
impl RegorusRvm {
fn new(vm: RegoVM) -> Self {
Self { vm: new_handle(vm) }
}
fn contention_error() -> anyhow::Error {
anyhow!("regorus rvm handle is already in use; create a separate VM per thread")
}
fn try_write(&self) -> Result<WriteGuard<'_, RegoVM>> {
try_write(&self.vm).ok_or_else(Self::contention_error)
}
fn try_read(&self) -> Result<ReadGuard<'_, RegoVM>> {
try_read(&self.vm).ok_or_else(Self::contention_error)
}
}
/// Drop a `RegorusProgram`.
#[no_mangle]
pub extern "C" fn regorus_program_drop(program: *mut RegorusProgram) {
if let Ok(program) = to_ref(program) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(program));
}
}
}
/// Drop a `RegorusRvm`.
#[no_mangle]
pub extern "C" fn regorus_rvm_drop(vm: *mut RegorusRvm) {
if let Ok(vm) = to_ref(vm) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(vm));
}
}
}
/// Compile a compiled policy into an RVM program.
///
/// * `compiled_policy` - Compiled policy handle
/// * `entry_points` - Array of entry point rule paths
/// * `entry_points_len` - Number of entry points
#[no_mangle]
pub extern "C" fn regorus_program_compile_from_policy(
compiled_policy: *mut RegorusCompiledPolicy,
entry_points: *const *const c_char,
entry_points_len: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusProgram> {
if entry_points.is_null() && entry_points_len > 0 {
return Err(anyhow!("null entry_points pointer"));
}
let mut entry_points_vec = Vec::with_capacity(entry_points_len);
for i in 0..entry_points_len {
unsafe {
let entry_ptr = entry_points.add(i);
if entry_ptr.is_null() {
return Err(anyhow!("null entry point at index {i}"));
}
let entry = from_c_str(*entry_ptr)?;
entry_points_vec.push(entry);
}
}
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let compiled_policy =
&to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?.compiled_policy;
let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(RegorusProgram { program })))
}();
match output {
Ok(program) => RegorusResult::ok_pointer(program as *mut c_void),
Err(err) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("RVM compilation failed: {err}"),
),
}
})
}
/// Compile an RVM program from data/modules and entry points.
///
/// * `data_json` - JSON string containing static data for policy evaluation
/// * `modules` - Array of policy modules to compile
/// * `modules_len` - Number of modules in the array
/// * `entry_points` - Array of entry point rule paths
/// * `entry_points_len` - Number of entry points
#[no_mangle]
pub extern "C" fn regorus_program_compile_from_modules(
data_json: *const c_char,
modules: *const RegorusPolicyModule,
modules_len: usize,
entry_points: *const *const c_char,
entry_points_len: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusProgram> {
if entry_points_len == 0 {
return Err(anyhow!("entry_points must contain at least one entry"));
}
let data_str = from_c_str(data_json)?;
let data = Value::from_json_str(&data_str)?;
let policy_modules = convert_c_modules_to_rust(modules, modules_len)?;
let entry_points_vec = convert_c_entry_points(entry_points, entry_points_len)?;
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let entry_rule = entry_points_ref
.first()
.ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?;
let compiled_policy = regorus::compile_policy_with_entrypoint(
data,
&policy_modules,
(*entry_rule).into(),
)?;
let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(RegorusProgram { program })))
}();
match output {
Ok(program) => RegorusResult::ok_pointer(program as *mut c_void),
Err(err) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("RVM compilation failed: {err}"),
),
}
})
}
/// Create a new, empty RVM program.
#[no_mangle]
pub extern "C" fn regorus_program_new() -> *mut RegorusProgram {
let program = Program::new();
Box::into_raw(Box::new(RegorusProgram {
program: Arc::new(program),
}))
}
/// Serialize a program to the binary RVM format.
#[no_mangle]
pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusBuffer> {
let program = &to_shared_ref(program as *const RegorusProgram)?.program;
let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?;
Ok(RegorusBuffer::from_vec(bytes))
}();
match output {
Ok(buffer) => RegorusResult::ok_pointer(buffer as *mut c_void),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Deserialize a program from the binary RVM format.
///
/// Returns a `RegorusProgram` handle and sets `is_partial` to true when the
/// program requires recompilation.
#[no_mangle]
pub extern "C" fn regorus_program_deserialize_binary(
data: *const u8,
len: usize,
is_partial: *mut bool,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<(*mut RegorusProgram, bool)> {
if data.is_null() {
if len > 0 {
return Err(anyhow!("null data pointer with non-zero length"));
}
return Err(anyhow!("null data pointer"));
}
let data = unsafe { core::slice::from_raw_parts(data, len) };
let (program, partial) =
match Program::deserialize_binary(data).map_err(|e| anyhow!(e))? {
DeserializationResult::Complete(program) => (program, false),
DeserializationResult::Partial(program) => (program, true),
};
Ok((
Box::into_raw(Box::new(RegorusProgram {
program: Arc::new(program),
})),
partial,
))
}();
match output {
Ok((program, partial)) => {
if !is_partial.is_null() {
unsafe {
*is_partial = partial;
}
}
RegorusResult::ok_pointer(program as *mut c_void)
}
Err(err) => {
RegorusResult::err_with_message(RegorusStatus::InvalidDataFormat, err.to_string())
}
}
})
}
/// Generate a default assembly listing for the program.
#[no_mangle]
pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let program = &to_shared_ref(program as *const RegorusProgram)?.program;
Ok(generate_assembly_listing(
program,
&AssemblyListingConfig::default(),
))
}();
match output {
Ok(listing) => RegorusResult::ok_string(listing),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Generate a tabular assembly listing for the program.
#[no_mangle]
pub extern "C" fn regorus_program_generate_tabular_listing(
program: *mut RegorusProgram,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let program = &to_shared_ref(program as *const RegorusProgram)?.program;
Ok(generate_tabular_assembly_listing(
program,
&AssemblyListingConfig::default(),
))
}();
match output {
Ok(listing) => RegorusResult::ok_string(listing),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Construct a new RVM instance.
#[no_mangle]
pub extern "C" fn regorus_rvm_new() -> *mut RegorusRvm {
Box::into_raw(Box::new(RegorusRvm::new(RegoVM::new())))
}
/// Construct a new RVM instance with a compiled policy for default rule evaluation.
#[no_mangle]
pub extern "C" fn regorus_rvm_new_with_policy(
compiled_policy: *mut RegorusCompiledPolicy,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusRvm> {
let policy = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?
.compiled_policy
.clone();
Ok(Box::into_raw(Box::new(RegorusRvm::new(
RegoVM::new_with_policy(policy),
))))
}();
match output {
Ok(vm) => RegorusResult::ok_pointer(vm as *mut c_void),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Load a program into the RVM.
#[no_mangle]
pub extern "C" fn regorus_rvm_load_program(
vm: *mut RegorusRvm,
program: *mut RegorusProgram,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let program = to_shared_ref(program as *const RegorusProgram)?
.program
.clone();
guard.load_program(program);
Ok(())
}())
})
}
/// Set the VM data document from JSON.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let data_value = Value::from_json_str(&from_c_str(data)?)?;
guard.set_data(data_value)?;
Ok(())
}())
})
}
/// Set the VM input document from JSON.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_input(
vm: *mut RegorusRvm,
input: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let input_value = Value::from_json_str(&from_c_str(input)?)?;
guard.set_input(input_value);
Ok(())
}())
})
}
/// Set the VM context document from JSON.
///
/// The context provides host-supplied ambient data (e.g. `resourceGroup()`,
/// `subscription()`) that Azure Policy functions can access via `LoadContext`
/// instructions. This must be called before `regorus_rvm_execute` when
/// evaluating policies that reference context functions.
///
/// # Safety
/// - `vm` must be a valid pointer to a `RegorusRvm` created by `regorus_rvm_new`.
/// - `context_json` must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_rvm_set_context(
vm: *mut RegorusRvm,
context_json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let context_value = Value::from_json_str(&from_c_str(context_json)?)?;
guard.set_context(context_value);
Ok(())
}())
})
}
/// Set the maximum number of instructions that can execute.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_max_instructions(
vm: *mut RegorusRvm,
max_instructions: usize,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
guard.set_max_instructions(max_instructions);
Ok(())
}())
})
}
/// Configure strict builtin error behavior.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_strict_builtin_errors(
vm: *mut RegorusRvm,
strict: bool,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
guard.set_strict_builtin_errors(strict);
Ok(())
}())
})
}
/// Configure the execution mode (0 = run-to-completion, 1 = suspendable).
#[no_mangle]
pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let mode = match mode {
0 => ExecutionMode::RunToCompletion,
1 => ExecutionMode::Suspendable,
_ => return Err(anyhow!("invalid execution mode: {mode}")),
};
guard.set_execution_mode(mode);
Ok(())
}())
})
}
/// Enable or disable step mode when running suspendable execution.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
guard.set_step_mode(enabled);
Ok(())
}())
})
}
/// Configure the per-VM execution timer override.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_execution_timer_config(
vm: *mut RegorusRvm,
has_config: bool,
config: RegorusExecutionTimerConfig,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
if has_config {
guard.set_execution_timer_config(Some(config.to_execution_timer_config()?));
} else {
guard.set_execution_timer_config(None);
}
Ok(())
}())
})
}
/// Execute the program's main entry point.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let result = guard.execute()?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Execute a named entry point.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute_entry_point_by_name(
vm: *mut RegorusRvm,
entry_point: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let name = from_c_str(entry_point)?;
let result = guard.execute_entry_point_by_name(&name)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Execute an entry point by index.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute_entry_point_by_index(
vm: *mut RegorusRvm,
index: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let result = guard.execute_entry_point_by_index(index)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Resume execution for suspendable runs.
#[no_mangle]
pub extern "C" fn regorus_rvm_resume(
vm: *mut RegorusRvm,
resume_value_json: *const c_char,
has_value: bool,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let value = if has_value {
Some(Value::from_json_str(&from_c_str(resume_value_json)?)?)
} else {
None
};
let result = guard.resume(value)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Get the current execution state of the VM.
#[no_mangle]
pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let guard = vm.try_read()?;
let state: ExecutionState = guard.execution_state().clone();
Ok(format!("{:?}", state))
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
fn convert_c_entry_points(
entry_points: *const *const c_char,
entry_points_len: usize,
) -> Result<Vec<String>> {
if entry_points.is_null() && entry_points_len > 0 {
return Err(anyhow!("null entry_points pointer"));
}
let mut entry_points_vec = Vec::with_capacity(entry_points_len);
for i in 0..entry_points_len {
unsafe {
let entry_ptr = entry_points.add(i);
if entry_ptr.is_null() {
return Err(anyhow!("null entry point at index {i}"));
}
let entry = from_c_str(*entry_ptr)?;
entry_points_vec.push(entry);
}
}
Ok(entry_points_vec)
}
fn convert_c_modules_to_rust(
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> Result<Vec<PolicyModule>> {
if modules.is_null() && modules_len > 0 {
return Err(anyhow!("null modules pointer"));
}
let mut policy_modules = Vec::with_capacity(modules_len);
for i in 0..modules_len {
unsafe {
let module = modules.add(i);
if module.is_null() {
return Err(anyhow!("null module at index {i}"));
}
let module_ref = &*module;
let id = from_c_str(module_ref.id)
.map_err(|e| anyhow!("invalid module id at index {i}: {e}"))?;
let content = from_c_str(module_ref.content)
.map_err(|e| anyhow!("invalid module content at index {i}: {e}"))?;
policy_modules.push(PolicyModule {
id: id.into(),
content: content.into(),
});
}
}
Ok(policy_modules)
}