I often find myself switching between unmanaged code in C, C++, and assembly - to managed code, written in C# with the .NET framework. Sometimes, there is cross-over between the two worlds, where I end up mixing managed code with unmanaged code. This is the tale of an issue I had while blending managed and unmanaged code.
I was developing a proof of concept using the managed wrapper for Network Monitor for a project that needed to demonstrate parsing of live network data and logging when certain anomalous network activity was identified. However, the proof of concept kept intermittently crashing when parsing received data. I wasn't finding anything obviously wrong when debugging the application in Visual Studio, so I decided to collect a dump file and dig into it with WinDbg. I found had some unusual results within WinDbg, where the unmanaged exception context in the dump showed the crash occurring in a stub method for Reverse PInvoke:
OS Thread Id: 0x2f8 (5)
Child SP IP Call Site
000000cd6d87f740 00007ff8abcb11fa [FaultingExceptionFrame: 000000cd6d87f740]
000000cd6d87fc90 00007ff8443d21c7 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int64, Int32, Int64, Int64)
Child SP IP Call Site
000000cd6d87f740 00007ff8abcb11fa [FaultingExceptionFrame: 000000cd6d87f740]
000000cd6d87fc90 00007ff8443d21c7 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int64, Int32, Int64, Int64)
This was my first time encountering a Reverse PInvoke, so I had to do some research to understand that method. I discovered that the Reverse PInvoke stub method is created when a managed method is passed as a callback to an unmanaged function. In other words, Reverse PInvoke is the proxy that bridges unmanaged code to managed code.
I was still not understanding the underlying cause of the crash, until I checked the managed exception context of the second dump file:
Exception type: System.NullReferenceException
Message: Object reference not set to an instance of an object.
InnerException: <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80004003 -> E_Pointer -> Pointer that is not valid
For some reason, this stub method was raising a NullReferenceException and the HResult revealed an invalid pointer was being passed. After digging around in the dump file (i.e. looking at the call stack and state of local objects) I still couldn’t figure out which pointer value was null, and why a managed exception was being raised. Since I couldn't find anything obviously wrong in the dump file, I finally just decided to decompile my code and see if I had stumbled on some rare, peculiar compiler bug. It wasn't a compiler bug, but the generated code was unexpected:
errno = NetmonAPI.NmConfigAdapter(
captureEngine,
i,
new NetworkMonitor.CaptureCallbackDelegate(CaptureTrafficCallback),
IntPtr.Zero,
NmCaptureCallbackExitMode.ReturnRemainFrames);
In the original code, this was written as such:
errno = NetmonAPI.NmConfigAdapter(
captureEngine,
i,
CaptureTrafficCallback,
IntPtr.Zero,
NmCaptureCallbackExitMode.ReturnRemainFrames);
i,
CaptureTrafficCallback,
IntPtr.Zero,
NmCaptureCallbackExitMode.ReturnRemainFrames);
This means that when invoking an unmanaged function with a static reference to a method, the compiler was generating a new instance of that method (my initial understanding was that this would work like to a function pointer in C). It's important to note that this new instance is passed to an unmanaged function, and not referenced anywhere else in the managed code. Therefore, after the function with the above code completes execution, I anticipated that the garbage collector must be cleaning up my callback instance! This would explain the intermittent crashes and the null pointer.
As a workaround, I refactored the managed callback instance to be stored as a private member in my class and the problem went away:
public class NetAnalyzer
{
CaptureCallbackDelegate captureCallback;
[...]
public void Init()
{
[...]
captureCallback = new CaptureCallbackDelegate(CaptureTrafficCallback);
errno = NetmonAPI.NmConfigAdapter(
captureEngine,
i,
captureCallback,
IntPtr.Zero,
NmCaptureCallbackExitMode.ReturnRemainFrames);
[...]
}
[...]
}