feat(ffi): unwind safety: shield FFI entrypoints with panic guard (#546)

This PR implements widely accepted Rust programming practices for
dealing with panics across ABI (programming language) boundaries.

- Add panic_guard.rs to wrap FFI calls and prevent panic across FFI/ABI boundary (undefined behavior).
- Capture per-thread backtraces via a temporary panic hook
- After a panic, subsequent invocations are poisoned.
- Integrate with_unwind_guard across the engine, schema registry, and target registry exportis

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-01-20 21:50:10 -06:00
committed by GitHub
parent 9426b2ec02
commit 80686d6ed1
21 changed files with 1008 additions and 501 deletions

View File

@@ -144,11 +144,11 @@ jobs:
path: ./bindings/csharp/regorus-nuget/
- name: Restore Regorus.Tests
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget /p:UseLocalRegorus=false
working-directory: ./bindings/csharp/Regorus.Tests
- name: Run Regorus.Tests
run: dotnet test --no-restore
run: dotnet test --no-restore -p:UseLocalRegorus=false
working-directory: ./bindings/csharp/Regorus.Tests
- name: Restore TestApp

View File

@@ -0,0 +1,84 @@
#if REGORUS_FFI_TEST_HOOKS
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus.Internal;
namespace Regorus.Tests;
[TestClass]
public sealed class PanicGuardTests
{
[TestInitialize]
public void Initialize()
{
API.regorus_engine_test_reset_poison();
}
[TestCleanup]
public void Cleanup()
{
API.regorus_engine_test_reset_poison();
}
[TestMethod]
public void Panic_produces_invalid_operation_exception()
{
var panic = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
StringAssert.Contains(panic.Message, "panicked", "panic message should capture payload");
}
[TestMethod]
public void Poison_flag_blocks_subsequent_calls()
{
_ = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
var poisoned = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
StringAssert.Contains(poisoned.Message, "poisoned", "poisoned message should explain guard state");
}
private static unsafe void TriggerPanic()
{
var result = API.regorus_engine_test_trigger_panic();
try
{
if (result.status == RegorusStatus.Ok)
{
return;
}
var message = PtrToStringUtf8((IntPtr)result.error_message);
throw result.status.CreateException(message);
}
finally
{
API.regorus_result_drop(result);
}
}
private static string? PtrToStringUtf8(IntPtr ptr)
{
#if NETSTANDARD2_1
return Marshal.PtrToStringUTF8(ptr);
#else
if (ptr == IntPtr.Zero)
{
return null;
}
var len = 0;
while (Marshal.ReadByte(ptr, len) != 0)
{
len++;
}
var buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, buffer.Length);
return System.Text.Encoding.UTF8.GetString(buffer);
#endif
}
}
#endif

View File

@@ -6,11 +6,17 @@
<!-- More info about dotnet test integration https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-integration-dotnet-test -->
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<!-- If the environment variable is set (such as in a Github Action run), append the suffix to the version number -->
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
<UseLocalRegorus Condition="'$(UseLocalRegorus)' == ''">true</UseLocalRegorus>
</PropertyGroup>
<PropertyGroup Condition="'$(UseLocalRegorus)' == 'true'">
<DefineConstants>$(DefineConstants);REGORUS_FFI_TEST_HOOKS</DefineConstants>
</PropertyGroup>
<ItemGroup>
@@ -21,7 +27,13 @@
<PackageReference Include="MSTest" Version="3.8.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)"/>
<ItemGroup Condition="'$(UseLocalRegorus)' == 'true'">
<ProjectReference Include="../Regorus/Regorus.csproj">
<AdditionalProperties>EnableRegorusTestHooks=true</AdditionalProperties>
</ProjectReference>
</ItemGroup>
<ItemGroup Condition="'$(UseLocalRegorus)' != 'true'">
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Regorus.Tests")]

View File

@@ -5,6 +5,7 @@ using System;
using System.Text;
using System.Text.Json;
using System.Threading;
using Regorus.Internal;
#nullable enable
namespace Regorus
@@ -165,7 +166,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type switch

View File

@@ -177,7 +177,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown compilation error occurred");
throw result.status.CreateException(message);
}
if (result.data_type != Internal.RegorusDataType.Pointer || result.pointer_value == null)

View File

@@ -373,21 +373,21 @@ namespace Regorus
string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
if (result.status != Regorus.Internal.RegorusStatus.Ok)
try
{
var message = StringFromUTF8((IntPtr)result.error_message);
var ex = new Exception(message);
Regorus.Internal.API.regorus_result_drop(result);
throw ex;
}
if (result.status != Regorus.Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw result.status.CreateException(message);
}
var resultString = "";
if (result.output is not null)
{
resultString = StringFromUTF8((IntPtr)result.output);
var output = result.output is not null ? StringFromUTF8((IntPtr)result.output) : null;
return output ?? string.Empty;
}
finally
{
Regorus.Internal.API.regorus_result_drop(result);
}
Regorus.Internal.API.regorus_result_drop(result);
return resultString;
}
private void ThrowIfDisposed()

View File

@@ -217,6 +217,20 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_compile_with_entrypoint(RegorusEngine* engine, byte* rule);
#if REGORUS_FFI_TEST_HOOKS
/// <summary>
/// Trigger a panic inside the engine for testing purposes.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_trigger_panic", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_test_trigger_panic();
/// <summary>
/// Reset the engine poison flag for testing.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_reset_poison", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_engine_test_reset_poison();
#endif
#endregion
#region Compilation Methods
@@ -472,6 +486,14 @@ namespace Regorus.Internal
/// Invalid policy content.
/// </summary>
InvalidPolicy,
/// <summary>
/// The engine panicked and cannot be reused until reset.
/// </summary>
Panic,
/// <summary>
/// The engine remains poisoned because a previous panic was detected.
/// </summary>
Poisoned,
}
/// <summary>

View File

@@ -17,6 +17,10 @@
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
<PropertyGroup Condition="'$(EnableRegorusTestHooks)' == 'true'">
<DefineConstants>$(DefineConstants);REGORUS_FFI_TEST_HOOKS</DefineConstants>
</PropertyGroup>
<!--
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in

View File

@@ -240,7 +240,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type switch
@@ -265,7 +265,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
@@ -283,7 +283,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
#nullable enable
namespace Regorus.Internal
{
internal static class StatusExtensions
{
internal static Exception CreateException(this RegorusStatus status, string? message)
{
var details = string.IsNullOrWhiteSpace(message) ? "Regorus call failed." : message;
return status switch
{
RegorusStatus.Panic => new InvalidOperationException($"Regorus engine panicked: {details}"),
RegorusStatus.Poisoned => new InvalidOperationException($"Regorus engine is poisoned: {details}"),
_ => new Exception(details),
};
}
}
}

View File

@@ -135,7 +135,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type switch
@@ -160,7 +160,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
@@ -178,7 +178,7 @@ namespace Regorus
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;

View File

@@ -34,6 +34,12 @@ pub enum RegorusStatus {
/// Invalid policy content.
InvalidPolicy,
/// The engine panicked and cannot be reused until reset.
Panic,
/// The engine remains poisoned because a previous panic was detected.
Poisoned,
}
/// Type of data contained in RegorusResult

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::vec::Vec;
@@ -43,55 +44,54 @@ pub extern "C" fn regorus_compile_policy_with_entrypoint(
modules_len: usize,
entry_point_rule: *const c_char,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
with_unwind_guard(|| {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
let entry_rule = match from_c_str(entry_point_rule) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidEntrypoint,
format!("Invalid entry point rule string: {e}"),
)
}
};
let entry_rule = match from_c_str(entry_point_rule) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidEntrypoint,
format!("Invalid entry point rule string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_with_entrypoint(data, &policy_modules, entry_rule.into()) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match compile_policy_with_entrypoint(data, &policy_modules, entry_rule.into()) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Policy compilation failed: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Policy compilation failed: {e}"),
),
}
})
}
/// Compiles a target-aware policy from data and modules.
@@ -122,45 +122,44 @@ pub extern "C" fn regorus_compile_policy_for_target(
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
with_unwind_guard(|| {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_for_target(data, &policy_modules) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match compile_policy_for_target(data, &policy_modules) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Target-aware policy compilation failed: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Target-aware policy compilation failed: {e}"),
),
}
})
}
/// Helper function to convert C module array to Rust Vec<PolicyModule>.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::common::*;
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::string::String;
use anyhow::Result;
@@ -35,18 +36,20 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
compiled_policy: *mut RegorusCompiledPolicy,
input: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)?
.compiled_policy
.eval_with_input(input_value)?;
result.to_json_str()
}();
with_unwind_guard(|| {
let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)?
.compiled_policy
.eval_with_input(input_value)?;
result.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Get information about the compiled policy including metadata about modules,
@@ -59,14 +62,16 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
pub extern "C" fn regorus_compiled_policy_get_policy_info(
compiled_policy: *mut RegorusCompiledPolicy,
) -> RegorusResult {
let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}();
with_unwind_guard(|| {
let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}

View File

@@ -8,6 +8,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
@@ -29,45 +30,45 @@ pub extern "C" fn regorus_effect_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid effect schema JSON string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid effect schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse effect schema JSON: {e}"),
)
}
};
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse effect schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::effect::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register effect schema: {e}"),
),
}
match schemas::effect::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register effect schema: {e}"),
),
}
})
}
/// Check if an effect schema with the given name exists.
@@ -83,18 +84,20 @@ pub extern "C" fn regorus_effect_schema_register(
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let contains = schemas::effect::contains(&schema_name);
RegorusResult::ok_bool(contains)
let contains = schemas::effect::contains(&schema_name);
RegorusResult::ok_bool(contains)
})
}
/// Get the number of registered effect schemas.
@@ -104,8 +107,10 @@ pub extern "C" fn regorus_effect_schema_contains(name: *const c_char) -> Regorus
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_len() -> RegorusResult {
let count = schemas::effect::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = schemas::effect::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the effect schema registry is empty.
@@ -115,8 +120,10 @@ pub extern "C" fn regorus_effect_schema_len() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_is_empty() -> RegorusResult {
let is_empty = schemas::effect::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = schemas::effect::is_empty();
RegorusResult::ok_bool(is_empty)
})
}
/// List all registered effect schema names as a JSON array.
@@ -126,14 +133,16 @@ pub extern "C" fn regorus_effect_schema_is_empty() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_list_names() -> RegorusResult {
let names = schemas::effect::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize effect schema names to JSON: {e}"),
),
}
with_unwind_guard(|| {
let names = schemas::effect::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize effect schema names to JSON: {e}"),
),
}
})
}
/// Remove an effect schema by name.
@@ -149,18 +158,20 @@ pub extern "C" fn regorus_effect_schema_list_names() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let removed = schemas::effect::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
let removed = schemas::effect::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
})
}
/// Clear all effect schemas from the registry.
@@ -170,6 +181,8 @@ pub extern "C" fn regorus_effect_schema_remove(name: *const c_char) -> RegorusRe
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_clear() -> RegorusResult {
schemas::effect::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
with_unwind_guard(|| {
schemas::effect::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
})
}

View File

@@ -6,6 +6,7 @@ use crate::common::{
};
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::lock::{new_handle, read, 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;
@@ -67,6 +68,101 @@ mod tests {
}
}
#[cfg(all(test, feature = "std"))]
mod panic_tests {
use super::{
regorus_engine_drop, regorus_engine_eval_query, regorus_engine_get_policies,
regorus_engine_new,
};
use crate::common::{regorus_result_drop, RegorusStatus};
use crate::panic_guard::{is_poisoned, reset_poison};
use alloc::boxed::Box;
use regorus::Value;
use std::ffi::{CStr, CString};
#[test]
fn catches_extension_panics_and_marks_poison() {
reset_poison();
let engine_ptr = regorus_engine_new();
assert!(!engine_ptr.is_null(), "engine allocation must succeed");
assert!(!is_poisoned(), "guard must start unpoisoned");
unsafe {
let engine = &mut *engine_ptr;
{
let mut guard = engine
.try_write()
.expect("exclusive access to configure engine");
guard
.add_extension(
"panic_extension".to_string(),
0,
Box::new(|_| -> anyhow::Result<Value> { panic!("ffi extension panic") }),
)
.expect("extension registration must succeed");
guard
.add_policy(
"panic.rego".to_string(),
"package panic\n\ndefault allow = false\n\nallow if {\n panic_extension()\n}"
.to_string(),
)
.expect("policy registration must succeed");
}
}
let query = CString::new("data.panic.allow").expect("valid query string");
let panic_result = regorus_engine_eval_query(engine_ptr, query.as_ptr());
assert!(matches!(panic_result.status, RegorusStatus::Panic));
unsafe {
assert!(
!panic_result.error_message.is_null(),
"panic details must be present"
);
let message = CStr::from_ptr(panic_result.error_message)
.to_str()
.expect("error message utf8");
assert!(
message.contains("ffi extension panic"),
"panic payload must bubble across guard"
);
}
regorus_result_drop(panic_result);
assert!(is_poisoned(), "engine must be marked poisoned after panic");
let poisoned_result = regorus_engine_get_policies(engine_ptr);
assert!(matches!(poisoned_result.status, RegorusStatus::Poisoned));
unsafe {
assert!(
!poisoned_result.error_message.is_null(),
"poison message must be present"
);
let message = CStr::from_ptr(poisoned_result.error_message)
.to_str()
.expect("poison message utf8");
assert!(
message.contains("regorus is poisoned"),
"poison message must inform callers"
);
}
regorus_result_drop(poisoned_result);
regorus_engine_drop(engine_ptr);
reset_poison();
}
}
#[no_mangle]
#[cfg(feature = "std")]
pub extern "C" fn regorus_engine_test_trigger_panic() -> RegorusResult {
with_unwind_guard(|| panic!("regorus ffi test panic"))
}
#[no_mangle]
pub extern "C" fn regorus_engine_test_reset_poison() {
crate::panic_guard::reset_poison();
}
#[no_mangle]
/// Construct a new Engine
///
@@ -116,11 +212,13 @@ pub extern "C" fn regorus_engine_add_policy(
path: *const c_char,
rego: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
})
}
#[cfg(feature = "std")]
@@ -129,11 +227,13 @@ pub extern "C" fn regorus_engine_add_policy_from_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy_from_file(from_c_str(path)?)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy_from_file(from_c_str(path)?)
}())
})
}
/// Add policy data.
@@ -145,11 +245,13 @@ pub extern "C" fn regorus_engine_add_data_json(
engine: *mut RegorusEngine,
data: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
})
}
/// Get list of loaded Rego packages as JSON.
@@ -157,11 +259,13 @@ pub extern "C" fn regorus_engine_add_data_json(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
#[no_mangle]
pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg)
}())
})
}
/// Get list of policies as JSON.
@@ -169,11 +273,13 @@ pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> Reg
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
#[no_mangle]
pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_policies_as_json()
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_policies_as_json()
}())
})
}
#[cfg(feature = "std")]
@@ -182,11 +288,13 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}())
})
}
/// Clear policy data.
@@ -194,12 +302,14 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
#[no_mangle]
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_data();
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_data();
Ok(())
}())
})
}
/// Set input.
@@ -211,12 +321,14 @@ pub extern "C" fn regorus_engine_set_input_json(
engine: *mut RegorusEngine,
input: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
})
}
#[cfg(feature = "std")]
@@ -225,12 +337,14 @@ pub extern "C" fn regorus_engine_set_input_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(())
}())
})
}
/// Evaluate query.
@@ -242,16 +356,18 @@ pub extern "C" fn regorus_engine_eval_query(
engine: *mut RegorusEngine,
query: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let results = guard.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let results = guard.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Evaluate specified rule.
@@ -263,15 +379,17 @@ pub extern "C" fn regorus_engine_eval_rule(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.eval_rule(from_c_str(rule)?)?.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.eval_rule(from_c_str(rule)?)?.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable coverage.
@@ -284,12 +402,14 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable);
Ok(())
}())
})
}
/// Get coverage report.
@@ -298,15 +418,17 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable strict builtin errors.
@@ -318,12 +440,14 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors(
engine: *mut RegorusEngine,
strict: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_strict_builtin_errors(strict);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_strict_builtin_errors(strict);
Ok(())
}())
})
}
/// Get pretty printed coverage report.
@@ -334,15 +458,17 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors(
pub extern "C" fn regorus_engine_get_coverage_report_pretty(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Clear coverage data.
@@ -351,12 +477,14 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty(
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_coverage_data();
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_coverage_data();
Ok(())
}())
})
}
/// Whether to gather output of print statements.
@@ -368,12 +496,14 @@ pub extern "C" fn regorus_engine_set_gather_prints(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_gather_prints(enable);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_gather_prints(enable);
Ok(())
}())
})
}
/// Take all the gathered print statements.
@@ -381,15 +511,17 @@ pub extern "C" fn regorus_engine_set_gather_prints(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
#[no_mangle]
pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
Ok(serde_json::to_string_pretty(&guard.take_prints()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
Ok(serde_json::to_string_pretty(&guard.take_prints()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Get AST of policies.
@@ -398,15 +530,17 @@ pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> Rego
#[no_mangle]
#[cfg(feature = "ast")]
pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_ast_as_json()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_ast_as_json()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Gets the package names defined in each policy added to the engine.
@@ -417,15 +551,18 @@ pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) ->
pub extern "C" fn regorus_engine_get_policy_package_names(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_package_names()?).map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_package_names()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Gets the parameters defined in each policy added to the engine.
@@ -436,15 +573,18 @@ pub extern "C" fn regorus_engine_get_policy_package_names(
pub extern "C" fn regorus_engine_get_policy_parameters(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_parameters()?).map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_parameters()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable rego v1.
@@ -455,16 +595,18 @@ pub extern "C" fn regorus_engine_set_rego_v0(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
let output = || -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Compile a target-aware policy from the current engine state.
@@ -476,37 +618,39 @@ pub extern "C" fn regorus_engine_set_rego_v0(
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult {
let engine = match to_ref(engine) {
Ok(engine) => engine,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Failed to get engine reference: {e}"),
)
}
};
with_unwind_guard(|| {
let engine = match to_ref(engine) {
Ok(engine) => engine,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Failed to get engine reference: {e}"),
)
}
};
let mut guard = match engine.try_write() {
Ok(guard) => guard,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to lock engine: {e}"),
)
}
};
let mut guard = match engine.try_write() {
Ok(guard) => guard,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to lock engine: {e}"),
)
}
};
match guard.compile_for_target() {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match guard.compile_for_target() {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile for target: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile for target: {e}"),
),
}
})
}
/// Compile a policy with a specific entry point rule.
@@ -520,23 +664,25 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into();
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy })
}();
with_unwind_guard(|| {
let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into();
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy })
}();
match result {
Ok(wrapped_policy) => {
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match result {
Ok(wrapped_policy) => {
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile with entrypoint: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile with entrypoint: {e}"),
),
}
})
}

View File

@@ -12,5 +12,6 @@ mod compiled_policy;
mod effect_registry;
mod engine;
mod lock;
mod panic_guard;
mod schema_registry;
mod target_registry;

View File

@@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Minimal helpers for catching panics inside the FFI layer.
//!
//! These are not yet wired into the exported functions; they will
//! be used once the integration work is complete.
extern crate alloc;
use crate::common::{RegorusResult, RegorusStatus};
use alloc::string::String;
use core::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "std")]
use std::{
backtrace::Backtrace,
cell::RefCell,
panic::{self, AssertUnwindSafe},
};
#[cfg(feature = "std")]
thread_local! {
// Stashes the formatted panic + backtrace for whichever call last panicked on this thread.
static PANIC_BACKTRACE: RefCell<Option<String>> = const { RefCell::new(None) };
}
#[cfg(feature = "std")]
type PanicHook = dyn Fn(&panic::PanicHookInfo<'_>) + Sync + Send + 'static;
#[cfg(feature = "std")]
/// RAII helper that installs a per-call panic hook and restores the prior hook on drop.
struct PanicHookGuard {
previous: Option<Box<PanicHook>>,
}
#[cfg(feature = "std")]
impl PanicHookGuard {
fn install() -> Self {
// Remember whatever hook the embedding application already registered.
let previous = panic::take_hook();
PANIC_BACKTRACE.with(|slot| {
slot.replace(None);
});
// Install our temporary hook so we can capture a backtrace for this invocation.
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::force_capture();
PANIC_BACKTRACE.with(|slot| {
slot.replace(Some(format!(
"panic hook observed: {}\nbacktrace:\n{:#?}",
info, backtrace
)));
});
}));
Self {
previous: Some(previous),
}
}
}
#[cfg(feature = "std")]
impl Drop for PanicHookGuard {
fn drop(&mut self) {
if let Some(previous) = self.previous.take() {
// Restore the original panic hook before we return control to the host.
panic::set_hook(previous);
}
}
}
static POISONED: AtomicBool = AtomicBool::new(false);
/// Result of attempting to run `f` while guarding against unwinding.
pub(crate) enum GuardResult<T> {
/// Closure completed successfully.
Success(T),
/// Closure panicked; contains a best-effort string payload.
Panic(String),
}
pub(crate) fn with_unwind_guard<F>(f: F) -> RegorusResult
where
F: FnOnce() -> RegorusResult,
{
if is_poisoned() {
return poisoned_result();
}
// The closure passed across this boundary closes over raw pointers and lock guards.
// These types are not unwind safe by default and may become poisoned if a panic occurs.
// We therefore use AssertUnwindSafe to get the compiler to accept the closure.
// Upon unwind, we mark regorus as poisoned and disallow further use.
#[cfg(feature = "std")]
{
let outcome = {
let _hook_guard = PanicHookGuard::install();
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => GuardResult::Success(value),
Err(payload) => GuardResult::Panic(panic_message_to_string(payload)),
}
};
finalize(outcome)
}
#[cfg(not(feature = "std"))]
return finalize(GuardResult::Success(f()));
}
fn finalize(outcome: GuardResult<RegorusResult>) -> RegorusResult {
match outcome {
GuardResult::Success(result) => result,
GuardResult::Panic(message) => {
trip_poison();
RegorusResult::err_with_message(RegorusStatus::Panic, message)
}
}
}
#[cfg(feature = "std")]
fn panic_message_to_string(payload: Box<dyn core::any::Any + Send + 'static>) -> String {
let mut message = if let Some(s) = payload.downcast_ref::<&str>() {
(*s).into()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
String::from("regorus encountered panic")
};
if let Some(backtrace) = take_panic_backtrace() {
message.push('\n');
message.push_str(&backtrace);
}
message
}
#[cfg(feature = "std")]
fn take_panic_backtrace() -> Option<String> {
PANIC_BACKTRACE.with(|slot| slot.borrow_mut().take())
}
fn poisoned_result() -> RegorusResult {
RegorusResult::err_with_message(
RegorusStatus::Poisoned,
String::from("regorus is poisoned after a previous panic"),
)
}
fn trip_poison() {
POISONED.store(true, Ordering::Release);
}
pub(crate) fn is_poisoned() -> bool {
POISONED.load(Ordering::Acquire)
}
pub(crate) fn reset_poison() {
POISONED.store(false, Ordering::Release);
}

View File

@@ -9,6 +9,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
@@ -32,45 +33,45 @@ pub extern "C" fn regorus_resource_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid schema JSON string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse schema JSON: {e}"),
)
}
};
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::resource::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register schema: {e}"),
),
}
match schemas::resource::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register schema: {e}"),
),
}
})
}
/// Check if a resource schema with the given name exists.
@@ -86,18 +87,20 @@ pub extern "C" fn regorus_resource_schema_register(
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let contains = schemas::resource::contains(&schema_name);
RegorusResult::ok_bool(contains)
let contains = schemas::resource::contains(&schema_name);
RegorusResult::ok_bool(contains)
})
}
/// Get the number of registered resource schemas.
@@ -107,8 +110,10 @@ pub extern "C" fn regorus_resource_schema_contains(name: *const c_char) -> Regor
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_len() -> RegorusResult {
let count = schemas::resource::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = schemas::resource::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the resource schema registry is empty.
@@ -118,8 +123,10 @@ pub extern "C" fn regorus_resource_schema_len() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_is_empty() -> RegorusResult {
let is_empty = schemas::resource::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = schemas::resource::is_empty();
RegorusResult::ok_bool(is_empty)
})
}
/// List all registered resource schema names as a JSON array.
@@ -129,14 +136,16 @@ pub extern "C" fn regorus_resource_schema_is_empty() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_list_names() -> RegorusResult {
let names = schemas::resource::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize schema names to JSON: {e}"),
),
}
with_unwind_guard(|| {
let names = schemas::resource::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize schema names to JSON: {e}"),
),
}
})
}
/// Remove a resource schema by name.
@@ -152,18 +161,20 @@ pub extern "C" fn regorus_resource_schema_list_names() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let removed = schemas::resource::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
let removed = schemas::resource::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
})
}
/// Clear all resource schemas from the registry.
@@ -173,6 +184,8 @@ pub extern "C" fn regorus_resource_schema_remove(name: *const c_char) -> Regorus
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_clear() -> RegorusResult {
schemas::resource::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
with_unwind_guard(|| {
schemas::resource::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
})
}

View File

@@ -4,6 +4,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::*;
use crate::panic_guard::with_unwind_guard;
use anyhow::Result;
use std::os::raw::c_char;
@@ -16,12 +17,14 @@ use std::os::raw::c_char;
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_register_target_from_json(target_json: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let target_str = from_c_str(target_json)?;
let target = regorus::Target::from_json_str(&target_str)?;
regorus::registry::targets::register(regorus::Rc::new(target))?;
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let target_str = from_c_str(target_json)?;
let target = regorus::Target::from_json_str(&target_str)?;
regorus::registry::targets::register(regorus::Rc::new(target))?;
Ok(())
}())
})
}
/// Check if a target is registered.
@@ -36,31 +39,35 @@ pub extern "C" fn regorus_register_target_from_json(target_json: *const c_char)
/// The name parameter must be a valid null-terminated UTF-8 string.
#[no_mangle]
pub extern "C" fn regorus_target_registry_contains(name: *const c_char) -> RegorusResult {
let target_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid target name string: {e}"),
)
}
};
with_unwind_guard(|| {
let target_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid target name string: {e}"),
)
}
};
let contains = regorus::registry::targets::contains(&target_name);
RegorusResult::ok_bool(contains)
let contains = regorus::registry::targets::contains(&target_name);
RegorusResult::ok_bool(contains)
})
}
/// Get a list of all registered target names as JSON array.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_list_names() -> RegorusResult {
let names = regorus::registry::targets::list_names();
let output = serde_json::to_string_pretty(&names).map_err(anyhow::Error::msg);
with_unwind_guard(|| {
let names = regorus::registry::targets::list_names();
let output = serde_json::to_string_pretty(&names).map_err(anyhow::Error::msg);
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Remove a target from the registry by name.
@@ -69,19 +76,23 @@ pub extern "C" fn regorus_target_registry_list_names() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_remove(name: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let name_str = from_c_str(name)?;
regorus::registry::targets::remove(&name_str);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let name_str = from_c_str(name)?;
regorus::registry::targets::remove(&name_str);
Ok(())
}())
})
}
/// Clear all targets from the registry.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_clear() -> RegorusResult {
regorus::registry::targets::clear();
RegorusResult::ok_void()
with_unwind_guard(|| {
regorus::registry::targets::clear();
RegorusResult::ok_void()
})
}
/// Get the number of registered targets.
@@ -91,8 +102,10 @@ pub extern "C" fn regorus_target_registry_clear() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_len() -> RegorusResult {
let count = regorus::registry::targets::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = regorus::registry::targets::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the target registry is empty.
@@ -102,6 +115,8 @@ pub extern "C" fn regorus_target_registry_len() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_is_empty() -> RegorusResult {
let is_empty = regorus::registry::targets::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = regorus::registry::targets::is_empty();
RegorusResult::ok_bool(is_empty)
})
}