mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
9426b2ec02
commit
80686d6ed1
84
bindings/csharp/Regorus.Tests/PanicGuardTests.cs
Normal file
84
bindings/csharp/Regorus.Tests/PanicGuardTests.cs
Normal 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
|
||||
@@ -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>
|
||||
3
bindings/csharp/Regorus/AssemblyInfo.cs
Normal file
3
bindings/csharp/Regorus/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Regorus.Tests")]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
24
bindings/csharp/Regorus/StatusExtensions.cs
Normal file
24
bindings/csharp/Regorus/StatusExtensions.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user