Skip to content

Commit 80686d6

Browse files
authored
feat(ffi): unwind safety: shield FFI entrypoints with panic guard (microsoft#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>
1 parent 9426b2e commit 80686d6

21 files changed

Lines changed: 1021 additions & 514 deletions

.github/workflows/test-csharp.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,11 @@ jobs:
144144
path: ./bindings/csharp/regorus-nuget/
145145

146146
- name: Restore Regorus.Tests
147-
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
147+
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget /p:UseLocalRegorus=false
148148
working-directory: ./bindings/csharp/Regorus.Tests
149149

150150
- name: Run Regorus.Tests
151-
run: dotnet test --no-restore
151+
run: dotnet test --no-restore -p:UseLocalRegorus=false
152152
working-directory: ./bindings/csharp/Regorus.Tests
153153

154154
- name: Restore TestApp
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#if REGORUS_FFI_TEST_HOOKS
2+
// Copyright (c) Microsoft Corporation.
3+
// Licensed under the MIT License.
4+
5+
using System;
6+
using System.Runtime.InteropServices;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using Regorus.Internal;
9+
10+
namespace Regorus.Tests;
11+
12+
[TestClass]
13+
public sealed class PanicGuardTests
14+
{
15+
[TestInitialize]
16+
public void Initialize()
17+
{
18+
API.regorus_engine_test_reset_poison();
19+
}
20+
21+
[TestCleanup]
22+
public void Cleanup()
23+
{
24+
API.regorus_engine_test_reset_poison();
25+
}
26+
27+
[TestMethod]
28+
public void Panic_produces_invalid_operation_exception()
29+
{
30+
var panic = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
31+
StringAssert.Contains(panic.Message, "panicked", "panic message should capture payload");
32+
}
33+
34+
[TestMethod]
35+
public void Poison_flag_blocks_subsequent_calls()
36+
{
37+
_ = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
38+
var poisoned = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
39+
StringAssert.Contains(poisoned.Message, "poisoned", "poisoned message should explain guard state");
40+
}
41+
42+
private static unsafe void TriggerPanic()
43+
{
44+
var result = API.regorus_engine_test_trigger_panic();
45+
try
46+
{
47+
if (result.status == RegorusStatus.Ok)
48+
{
49+
return;
50+
}
51+
52+
var message = PtrToStringUtf8((IntPtr)result.error_message);
53+
throw result.status.CreateException(message);
54+
}
55+
finally
56+
{
57+
API.regorus_result_drop(result);
58+
}
59+
}
60+
61+
private static string? PtrToStringUtf8(IntPtr ptr)
62+
{
63+
#if NETSTANDARD2_1
64+
return Marshal.PtrToStringUTF8(ptr);
65+
#else
66+
if (ptr == IntPtr.Zero)
67+
{
68+
return null;
69+
}
70+
71+
var len = 0;
72+
while (Marshal.ReadByte(ptr, len) != 0)
73+
{
74+
len++;
75+
}
76+
77+
var buffer = new byte[len];
78+
Marshal.Copy(ptr, buffer, 0, buffer.Length);
79+
return System.Text.Encoding.UTF8.GetString(buffer);
80+
#endif
81+
}
82+
}
83+
84+
#endif

bindings/csharp/Regorus.Tests/Regorus.Tests.csproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
<!-- More info about dotnet test integration https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-integration-dotnet-test -->
77
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
88
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
9+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
910
</PropertyGroup>
1011

1112
<PropertyGroup>
1213
<!-- If the environment variable is set (such as in a Github Action run), append the suffix to the version number -->
1314
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
15+
<UseLocalRegorus Condition="'$(UseLocalRegorus)' == ''">true</UseLocalRegorus>
16+
</PropertyGroup>
17+
18+
<PropertyGroup Condition="'$(UseLocalRegorus)' == 'true'">
19+
<DefineConstants>$(DefineConstants);REGORUS_FFI_TEST_HOOKS</DefineConstants>
1420
</PropertyGroup>
1521

1622
<ItemGroup>
@@ -21,7 +27,13 @@
2127
<PackageReference Include="MSTest" Version="3.8.2" />
2228
</ItemGroup>
2329

24-
<ItemGroup>
25-
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)"/>
30+
<ItemGroup Condition="'$(UseLocalRegorus)' == 'true'">
31+
<ProjectReference Include="../Regorus/Regorus.csproj">
32+
<AdditionalProperties>EnableRegorusTestHooks=true</AdditionalProperties>
33+
</ProjectReference>
34+
</ItemGroup>
35+
36+
<ItemGroup Condition="'$(UseLocalRegorus)' != 'true'">
37+
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)" />
2638
</ItemGroup>
2739
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Regorus.Tests")]

bindings/csharp/Regorus/CompiledPolicy.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using System.Text.Json;
77
using System.Threading;
8+
using Regorus.Internal;
89

910
#nullable enable
1011
namespace Regorus
@@ -165,7 +166,7 @@ private void ThrowIfDisposed()
165166
if (result.status != Internal.RegorusStatus.Ok)
166167
{
167168
var message = StringFromUTF8((IntPtr)result.error_message);
168-
throw new Exception(message ?? "Unknown error occurred");
169+
throw result.status.CreateException(message);
169170
}
170171

171172
return result.data_type switch

bindings/csharp/Regorus/Compiler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ private static CompiledPolicy GetCompiledPolicyResult(Internal.RegorusResult res
177177
if (result.status != Internal.RegorusStatus.Ok)
178178
{
179179
var message = StringFromUTF8((IntPtr)result.error_message);
180-
throw new Exception(message ?? "Unknown compilation error occurred");
180+
throw result.status.CreateException(message);
181181
}
182182

183183
if (result.data_type != Internal.RegorusDataType.Pointer || result.pointer_value == null)

bindings/csharp/Regorus/Engine.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -373,21 +373,21 @@ public void SetGatherPrints(bool enable)
373373

374374
string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
375375
{
376-
if (result.status != Regorus.Internal.RegorusStatus.Ok)
376+
try
377377
{
378-
var message = StringFromUTF8((IntPtr)result.error_message);
379-
var ex = new Exception(message);
380-
Regorus.Internal.API.regorus_result_drop(result);
381-
throw ex;
382-
}
378+
if (result.status != Regorus.Internal.RegorusStatus.Ok)
379+
{
380+
var message = StringFromUTF8((IntPtr)result.error_message);
381+
throw result.status.CreateException(message);
382+
}
383383

384-
var resultString = "";
385-
if (result.output is not null)
384+
var output = result.output is not null ? StringFromUTF8((IntPtr)result.output) : null;
385+
return output ?? string.Empty;
386+
}
387+
finally
386388
{
387-
resultString = StringFromUTF8((IntPtr)result.output);
389+
Regorus.Internal.API.regorus_result_drop(result);
388390
}
389-
Regorus.Internal.API.regorus_result_drop(result);
390-
return resultString;
391391
}
392392

393393
private void ThrowIfDisposed()

bindings/csharp/Regorus/NativeMethods.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ internal static unsafe partial class API
217217
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
218218
internal static extern RegorusResult regorus_engine_compile_with_entrypoint(RegorusEngine* engine, byte* rule);
219219

220+
#if REGORUS_FFI_TEST_HOOKS
221+
/// <summary>
222+
/// Trigger a panic inside the engine for testing purposes.
223+
/// </summary>
224+
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_trigger_panic", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
225+
internal static extern RegorusResult regorus_engine_test_trigger_panic();
226+
227+
/// <summary>
228+
/// Reset the engine poison flag for testing.
229+
/// </summary>
230+
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_reset_poison", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
231+
internal static extern void regorus_engine_test_reset_poison();
232+
#endif
233+
220234
#endregion
221235

222236
#region Compilation Methods
@@ -472,6 +486,14 @@ internal enum RegorusStatus : uint
472486
/// Invalid policy content.
473487
/// </summary>
474488
InvalidPolicy,
489+
/// <summary>
490+
/// The engine panicked and cannot be reused until reset.
491+
/// </summary>
492+
Panic,
493+
/// <summary>
494+
/// The engine remains poisoned because a previous panic was detected.
495+
/// </summary>
496+
Poisoned,
475497
}
476498

477499
/// <summary>

bindings/csharp/Regorus/Regorus.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<PackageReference Include="System.Text.Json" Version="8.0.5" />
1818
</ItemGroup>
1919

20+
<PropertyGroup Condition="'$(EnableRegorusTestHooks)' == 'true'">
21+
<DefineConstants>$(DefineConstants);REGORUS_FFI_TEST_HOOKS</DefineConstants>
22+
</PropertyGroup>
23+
2024
<!--
2125
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
2226
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in

bindings/csharp/Regorus/SchemaRegistry.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public static void ClearEffects()
240240
if (result.status != Internal.RegorusStatus.Ok)
241241
{
242242
var message = StringFromUTF8((IntPtr)result.error_message);
243-
throw new Exception(message ?? "Unknown error occurred");
243+
throw result.status.CreateException(message);
244244
}
245245

246246
return result.data_type switch
@@ -265,7 +265,7 @@ private static bool GetBoolResult(Internal.RegorusResult result)
265265
if (result.status != Internal.RegorusStatus.Ok)
266266
{
267267
var message = StringFromUTF8((IntPtr)result.error_message);
268-
throw new Exception(message ?? "Unknown error occurred");
268+
throw result.status.CreateException(message);
269269
}
270270

271271
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
@@ -283,7 +283,7 @@ private static long GetIntResult(Internal.RegorusResult result)
283283
if (result.status != Internal.RegorusStatus.Ok)
284284
{
285285
var message = StringFromUTF8((IntPtr)result.error_message);
286-
throw new Exception(message ?? "Unknown error occurred");
286+
throw result.status.CreateException(message);
287287
}
288288

289289
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;

0 commit comments

Comments
 (0)