Search code examples
c#delegates.net-6.0system.reflectioneventhandler

NullReferenceException thrown but the object passed the null check, how is that possible?


I'm using the AddEventHandler method from that answer, but when doing it on an EventHandler with a value type argument that happens:

using System.Reflection;

public class Program
{
    public static event EventHandler<bool> MyEvent;

    public static void Main()
    {
        EventInfo eventInfo = typeof(Program).GetEvent(nameof(MyEvent));
        AddEventHandler(eventInfo, null, (s, e) => {
            if (e == null) return; // either if condition or null conditional operator
            Console.WriteLine(e?.ToString());
        });
        MyEvent(null, true);
    }

    public static void AddEventHandler(EventInfo eventInfo, object client, EventHandler handler)
    {
        object eventInfoHandler = eventInfo.EventHandlerType
            .GetConstructor(new[] { typeof(object), typeof(IntPtr) })
            .Invoke(new[] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });

        eventInfo.AddEventHandler(client, (Delegate)eventInfoHandler);
    }
}

enter image description here

Any explanation?


Solution

  • You are using undocumented, internal api, and what's even worse is that this api accepts raw pointer. So it's not surprising if things go (horribly) wrong if you misuse such api (and you cannot ever be sure you are using it correctly because it's not documented).

    Note that AddEventHandler third parameter is EventHandler, which is delegate of this type:

    delegate void EventHandler(object sender, EventArgs e);
    

    And your MyEvent delegate type is:

    delegate void EventHandler(object sender, int e);
    

    AddEventHandler uses internal undocumented compiler-generated constructor of delegate which accepts two parameters: delegate target and raw pointer to method. It then just passes raw pointer to the method of handler delegate to that constructor without doing any checks. Delegate you pass and delegate being created can be completely incompatible, but you won't notice that until runtime will try to invoke it.

    In this case, runtime thinks it has delegate pointing to method void (object, bool), but actually it points to method void (object, EventArgs). You call it via MyEvent(null, true) and runtime passes true boolean value as second argument (it's value type so value is passed directly), but your delegate actually points to method which expects EventArgs, which is a reference type. So it expects an address of some object of type EventArgs. It gets boolean value as if it was pointer to EventArgs.

    Now, == null in general case basically just checks if the reference is zero (all bytes are 0). True boolean value is not represented by 0, so null check passes.

    Then, it tries to access object located at this "address". It cannot work, you access protected memory and get access violation error. However, as explained in this answer:

    but if (A) the access violation happened at an address lower than 0x00010000 and (B) such a violation is found to have happened by code that was jitted, then it is turned into a NullReferenceException, otherwise it gets turned into an AccessViolationException

    So it is turned into NullReferenceException you observe.

    Interesting that if you change your code like this:

    MyEvent(null, false);
    

    Then it will run without errors, because false is represented by zero byte, and so e == null check will return true.

    You can play with this code a bit more, for example change event type to int:

    public static event EventHandler<int> MyEvent; 
    

    And then do:

    MyEvent(null, 0x00010001);
    

    Now it will throw AccessViolationException instead of NullReferenceException as the linked answer claims (now we are trying to access memory at location higher than 0x00010000 so runtime does not convert this access violation into null reference exception).

    Here is another fun thing, we are using code from this answer to obtain memory address of .NET object at runtime, then pass that address into handler:

    public static event EventHandler<IntPtr> MyEvent;
    
    public static unsafe void Main() {
        // it's not even EventArgs, it's string
        var fakeArgument = "Hello world!";
        // some black magic to get address
        var typedRef = __makeref(fakeArgument);
        IntPtr ptr = **(IntPtr**)(&typedRef);
        EventInfo eventInfo = typeof(Program).GetEvent(nameof(MyEvent));
        AddEventHandler(eventInfo, null, (object s, EventArgs e) => {
            if (e == null) return;
            // e is actually a string here, not EventArgs...
            Console.WriteLine(e?.ToString());
        });
        MyEvent(null,  ptr);
    }
    

    This code outputs "Hello world!", for the reasons explained above.

    So long story short - don't use such dangerous undocumented internal apis in real code.