CppSharp: [Regression] Access violation when implementing native interface by managed class

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 😃. image

The issue seems to origin at 09222174c1a71445e1ca2d (Made the original virtual tables static too) where initialization timing of VTable changed from lazy-based-on-destructorOnly to eager-regardless-of-destructorOnly: image https://github.com/mono/CppSharp/commit/09222174c1a71445e1ca2debc654f984afae47c0#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. image

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.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 20 (10 by maintainers)

Most upvoted comments

This report is… breathtaking. We usually suggest to users to contact our support for a quick resolution but the effort you’ve put in is too impressive. I have an ongoing issue to solve in a day or two and I promise yours is next.