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

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