-
Notifications
You must be signed in to change notification settings - Fork 531
Description
Brief Description
After update from CppSharp 0.10.2 to 1.0.0 we've noticed that semantics of virtual table substitution in generated C# changed slightly, causing Access violation (reads of random memory) upon instantiation of managed implementation of native interface (this is mouthful, rather see code below :).
The issue seems to origin at 0922217 (Made the original virtual tables static too) where initialization timing of VTable changed from lazy-based-on-destructorOnly to eager-regardless-of-destructorOnly:
0922217#diff-b771f00937690119aa0c90ea5fece77f0215bf232a7b3273d7729de184083ed8R1570
Fast-forward though all patches to current version, this ultimately changes generated code so that full VTable is read from instance
regardless if it's actually present or not.
Should the first call to SetupVTables
come from arg-less/default managed ctor of managed implementation of native interface, crash occurs.
Used headers and generated code
//Native header (Simple pure virtual class/interface)
class EXPORT MethodDiagnosticListener
{
public:
NO_COPY_MOVE(MethodDiagnosticListener)
MethodDiagnosticListener() = default;
virtual ~MethodDiagnosticListener() = default;
virtual void HandleDiagnostics(int severity, InteropString diagnostics, Loc location) = 0;
};
//C# class implementing the native interface
public class DiagnosticsListener : MethodDiagnosticListener
{
public override void HandleDiagnostics(int severity, InteropString diagnostics, Loc location)
{ }
}
//CppSharp generated C# (stripped to relevant parts)
public unsafe abstract partial class MethodDiagnosticListener : IDisposable
{
[StructLayout(LayoutKind.Sequential, Size = 8)]
public partial struct __Internal
{
internal __IntPtr vfptr_MethodDiagnosticListener;
}
public __IntPtr __Instance { get; protected set; }
internal static readonly global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener> NativeToManagedMap = new global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener>();
protected bool __ownsNativeInstance;
// DEBUG: MethodDiagnosticListener() = default
protected MethodDiagnosticListener()
{
__Instance = Marshal.AllocHGlobal(sizeof(global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.__Internal));
__ownsNativeInstance = true;
NativeToManagedMap[__Instance] = this;
SetupVTables(GetType().FullName == "TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener");
}
// DEBUG: virtual void HandleDiagnostics(int severity, InteropString diagnostics, Loc location) = 0
public abstract void HandleDiagnostics(int severity, global::TandemSharp.TandemCompiler.InteropString diagnostics, global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.Loc location);
#region Virtual table interop
// virtual ~MethodDiagnosticListener() = default
private static global::TandemSharp.Delegates.Action___IntPtr_int _dtorDelegateInstance;
private static void _dtorDelegateHook(__IntPtr __instance, int delete)
{
var __target = global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.__GetInstance(__instance);
__target.Dispose(disposing: true, callNativeDtor: true);
}
// void HandleDiagnostics(int severity, InteropString diagnostics, Loc location) = 0
private static global::TandemSharp.Delegates.Action___IntPtr_int_TandemSharp_TandemCompiler_InteropString___Internal___IntPtr _HandleDiagnosticsDelegateInstance;
private static void _HandleDiagnosticsDelegateHook(__IntPtr __instance, int severity, global::TandemSharp.TandemCompiler.InteropString.__Internal diagnostics, __IntPtr location)
{
var __target = global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.__GetInstance(__instance);
var __result2 = location != IntPtr.Zero ? global::TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.Loc.__CreateInstance(location) : default;
__target.HandleDiagnostics(severity, global::TandemSharp.TandemCompiler.InteropString.__CreateInstance(diagnostics), __result2);
}
internal static class VTableLoader
{
private static volatile bool initialized;
private static readonly IntPtr*[] ManagedVTables = new IntPtr*[1];
private static readonly IntPtr*[] ManagedVTablesDtorOnly = new IntPtr*[1];
private static readonly IntPtr[] Thunks = new IntPtr[2];
private static CppSharp.Runtime.VTables VTables;
private static readonly global::System.Collections.Generic.List<CppSharp.Runtime.SafeUnmanagedMemoryHandle>
SafeHandles = new global::System.Collections.Generic.List<CppSharp.Runtime.SafeUnmanagedMemoryHandle>();
static VTableLoader()
{
_dtorDelegateInstance += _dtorDelegateHook;
_HandleDiagnosticsDelegateInstance += _HandleDiagnosticsDelegateHook;
Thunks[0] = Marshal.GetFunctionPointerForDelegate(_dtorDelegateInstance);
Thunks[1] = Marshal.GetFunctionPointerForDelegate(_HandleDiagnosticsDelegateInstance);
}
public static CppSharp.Runtime.VTables SetupVTables(IntPtr instance, bool destructorOnly = false)
{
if (!initialized)
{
lock (ManagedVTables)
{
if (!initialized)
{
initialized = true;
VTables.Tables = new IntPtr[] { *(IntPtr*)(instance + 0) };
VTables.Methods = new Delegate[1][];
ManagedVTablesDtorOnly[0] = CppSharp.Runtime.VTables.CloneTable(SafeHandles, instance, 0, 2);
ManagedVTablesDtorOnly[0][0] = Thunks[0];
ManagedVTables[0] = CppSharp.Runtime.VTables.CloneTable(SafeHandles, instance, 0, 2);
ManagedVTables[0][0] = Thunks[0];
ManagedVTables[0][1] = Thunks[1];
VTables.Methods[0] = new Delegate[2];
}
}
}
if (destructorOnly)
{
*(IntPtr**)(instance + 0) = ManagedVTablesDtorOnly[0];
}
else
{
*(IntPtr**)(instance + 0) = ManagedVTables[0];
}
return VTables;
}
}
protected CppSharp.Runtime.VTables __vtables;
internal virtual CppSharp.Runtime.VTables __VTables
{
get {
if (__vtables.IsEmpty)
__vtables.Tables = new IntPtr[] { *(IntPtr*)(__Instance + 0) };
return __vtables;
}
set {
__vtables = value;
}
}
internal virtual void SetupVTables(bool destructorOnly = false)
{
if (__VTables.IsTransient)
__VTables = VTableLoader.SetupVTables(__Instance, destructorOnly);
}
#endregion
}
Crash occurs upon DiagnosticsListener
instantiation
new DiagnosticsListener();
Unless there is some fundamental misconfiguration of the generator on our side, to my understanding this is supported scenario broken by an unfortunate regression, for which I suggest following update to the VTableLoader
to address the issue:
internal static class VTableLoader
{
private static volatile IntPtr*[] ManagedVTables;
private static volatile IntPtr*[] ManagedVTablesDtorOnly;
private static readonly IntPtr[] Thunks = new IntPtr[2];
private static CppSharp.Runtime.VTables VTables;
private static readonly global::System.Collections.Generic.List<CppSharp.Runtime.SafeUnmanagedMemoryHandle>
SafeHandles = new global::System.Collections.Generic.List<CppSharp.Runtime.SafeUnmanagedMemoryHandle>();
static VTableLoader()
{
_dtorDelegateInstance += _dtorDelegateHook;
_HandleDiagnosticsDelegateInstance += _HandleDiagnosticsDelegateHook;
Thunks[0] = Marshal.GetFunctionPointerForDelegate(_dtorDelegateInstance);
Thunks[1] = Marshal.GetFunctionPointerForDelegate(_HandleDiagnosticsDelegateInstance);
}
public static CppSharp.Runtime.VTables SetupVTables(IntPtr instance, bool destructorOnly = false)
{
if (destructorOnly)
{
if(ManagedVTablesDtorOnly is null)
{
lock (SafeHandles)
{
if(ManagedVTablesDtorOnly is null)
{
IntPtr*[] vTables = CppSharp.Runtime.VTables.AllocateTable(SafeHandles, 2);
CppSharp.Runtime.VTables.CloneTable(vTables, instance, 0, 2);
vTables[0][0] = Thunks[0];
//Proper release barrier to fix race condition that's present in current code
ManagedVTablesDtorOnly = vTables;
}
}
}
*(IntPtr**)(instance + 0) = ManagedVTablesDtorOnly[0];
}
else
{
if(ManagedVTables is null)
{
lock (SafeHandles)
{
if(ManagedVTables is null)
{
IntPtr*[] vTables = CppSharp.Runtime.VTables.AllocateTable(SafeHandles, 2);
vTables[0][0] = Thunks[0];
vTables[0][1] = Thunks[1];
//Proper release barrier to fix race condition that's present in current code
ManagedVTables = vTables;
}
}
}
*(IntPtr**)(instance + 0) = ManagedVTables[0];
}
//TODO: Handle `VTables.Tables` and `VTables.Methods`, should be initialized only in `destructorOnly`, but I'm not sure about that
return VTables;
}
}
Used settings
Target: MSVC
OS: Windows (will reproduce on any platform)
This is our setup function
public void Setup(Driver driver)
{
driver.ParserOptions.TargetTriple = "x86_64-pc-windows-msvc";
driver.ParserOptions.LanguageVersion = LanguageVersion.CPP17;
driver.ParserOptions.SetupMSVC();
driver.ParserOptions.EnableRTTI = true;
var options = driver.Options;
options.GeneratorKind = GeneratorKind.CSharp;
}
Stack trace
mscorlib.dll!System.Buffer.Memmove(byte* dest, byte* src, ulong len) Unknown No symbols loaded.
> TandemSharp.dll!CppSharp.Runtime.VTables.CloneTable(System.Collections.Generic.List<CppSharp.Runtime.SafeUnmanagedMemoryHandle> cache, System.IntPtr instance, int offset, int size) Line 55 C# Symbols loaded.
TandemSharp.dll!TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.VTableLoader.SetupVTables(System.IntPtr instance, bool destructorOnly) Line 570 C# Symbols loaded.
TandemSharp.dll!TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.SetupVTables(bool destructorOnly) Line 609 C# Symbols loaded.
TandemSharp.dll!TandemSharp.TandemCompiler.Debug.MethodDiagnosticListener.MethodDiagnosticListener() Line 495 C# Symbols loaded.