Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

Description

@ghost

Consider the following test app:

namespace ConsoleApp1
{
public unsafe class GG
{
public static void Main()
{
Test(UIntPtr.Zero);
}
public static void Test(object parameterToPassIn)
{
MyBinder binder = new MyBinder();
MethodInfo m = typeof(GG).GetMethod("Foo");
object[] args = { parameterToPassIn };
m.Invoke(null, BindingFlags.Default, binder, args, null);
if (!binder.ChangeTypeCalled)
{
Console.WriteLine("Failed. Binder not given chance to fix things.");
return;
}
Console.WriteLine("Passed.");
return;
}
public static void Foo(void*pv)
{
}
}
public sealed unsafe class MyBinder : Binder
{
public bool ChangeTypeCalled { get; private set; }
public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
{
throw new NotImplementedException();
}
public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
{
throw new NotImplementedException();
}
public sealed override object ChangeType(object value, Type type, CultureInfo culture)
{
ChangeTypeCalled = true;
return Pointer.Box((void*)0, typeof(void*));
}
public sealed override void ReorderArgumentArray(ref object[] args, object state)
{
throw new NotImplementedException();
}
public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
{
throw new NotImplementedException();
}
public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
{
throw new NotImplementedException();
}
}
}

Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

That is, on release builds.

On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

GC_TRIGGERS encountered in a GC_NOTRIGGER scope
CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)

The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

 case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
{
// If we got the univeral zero...Then assign it and exit.
if (rObj == 0) {
*(PVOID *)pArgDst = 0;
}
else {
if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
ARG_SLOT slot;
CreatePrimitiveValue(oType, oType, rObj, &slot);
*(PVOID *)pArgDst = (PVOID)slot;
}
else
COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
}
break;
}

Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

Metadata

Metadata

Assignees

Type

No type

Projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

    Description

    @ghost

    Consider the following test app:

    namespace ConsoleApp1
    {
    public unsafe class GG
    {
    public static void Main()
    {
    Test(UIntPtr.Zero);
    }
    public static void Test(object parameterToPassIn)
    {
    MyBinder binder = new MyBinder();
    MethodInfo m = typeof(GG).GetMethod("Foo");
    object[] args = { parameterToPassIn };
    m.Invoke(null, BindingFlags.Default, binder, args, null);
    if (!binder.ChangeTypeCalled)
    {
    Console.WriteLine("Failed. Binder not given chance to fix things.");
    return;
    }
    Console.WriteLine("Passed.");
    return;
    }
    public static void Foo(void*pv)
    {
    }
    }
    public sealed unsafe class MyBinder : Binder
    {
    public bool ChangeTypeCalled { get; private set; }
    public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
    {
    throw new NotImplementedException();
    }
    public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
    {
    throw new NotImplementedException();
    }
    public sealed override object ChangeType(object value, Type type, CultureInfo culture)
    {
    ChangeTypeCalled = true;
    return Pointer.Box((void*)0, typeof(void*));
    }
    public sealed override void ReorderArgumentArray(ref object[] args, object state)
    {
    throw new NotImplementedException();
    }
    public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
    {
    throw new NotImplementedException();
    }
    public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
    {
    throw new NotImplementedException();
    }
    }
    }
    

    Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

    Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

    That is, on release builds.

    On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

    GC_TRIGGERS encountered in a GC_NOTRIGGER scope
    CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
    GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
    CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
    GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
    CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
    GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
    CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
    CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
    CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
    CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
    CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
    CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
    CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
    CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
    NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
    CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
    

    The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

    However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

     case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
    {
    // If we got the univeral zero...Then assign it and exit.
    if (rObj == 0) {
    *(PVOID *)pArgDst = 0;
    }
    else {
    if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
    else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
    ARG_SLOT slot;
    CreatePrimitiveValue(oType, oType, rObj, &slot);
    *(PVOID *)pArgDst = (PVOID)slot;
    }
    else
    COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
    }
    break;
    }
    

    Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

    Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

    Metadata

    Metadata

    Assignees

    Type

    No type

    Projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

      Description

      @ghost

      Consider the following test app:

      namespace ConsoleApp1
      {
      public unsafe class GG
      {
      public static void Main()
      {
      Test(UIntPtr.Zero);
      }
      public static void Test(object parameterToPassIn)
      {
      MyBinder binder = new MyBinder();
      MethodInfo m = typeof(GG).GetMethod("Foo");
      object[] args = { parameterToPassIn };
      m.Invoke(null, BindingFlags.Default, binder, args, null);
      if (!binder.ChangeTypeCalled)
      {
      Console.WriteLine("Failed. Binder not given chance to fix things.");
      return;
      }
      Console.WriteLine("Passed.");
      return;
      }
      public static void Foo(void*pv)
      {
      }
      }
      public sealed unsafe class MyBinder : Binder
      {
      public bool ChangeTypeCalled { get; private set; }
      public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
      {
      throw new NotImplementedException();
      }
      public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
      {
      throw new NotImplementedException();
      }
      public sealed override object ChangeType(object value, Type type, CultureInfo culture)
      {
      ChangeTypeCalled = true;
      return Pointer.Box((void*)0, typeof(void*));
      }
      public sealed override void ReorderArgumentArray(ref object[] args, object state)
      {
      throw new NotImplementedException();
      }
      public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
      {
      throw new NotImplementedException();
      }
      public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
      {
      throw new NotImplementedException();
      }
      }
      }
      

      Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

      Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

      That is, on release builds.

      On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

      GC_TRIGGERS encountered in a GC_NOTRIGGER scope
      CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
      GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
      CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
      GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
      CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
      GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
      CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
      CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
      CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
      CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
      CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
      CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
      CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
      CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
      NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
      CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
      

      The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

      However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

       case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
      {
      // If we got the univeral zero...Then assign it and exit.
      if (rObj == 0) {
      *(PVOID *)pArgDst = 0;
      }
      else {
      if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
      else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
      ARG_SLOT slot;
      CreatePrimitiveValue(oType, oType, rObj, &slot);
      *(PVOID *)pArgDst = (PVOID)slot;
      }
      else
      COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
      }
      break;
      }
      

      Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

      Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

      Metadata

      Metadata

      Assignees

      Type

      No type

      Projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

        Description

        @ghost

        Consider the following test app:

        namespace ConsoleApp1
        {
        public unsafe class GG
        {
        public static void Main()
        {
        Test(UIntPtr.Zero);
        }
        public static void Test(object parameterToPassIn)
        {
        MyBinder binder = new MyBinder();
        MethodInfo m = typeof(GG).GetMethod("Foo");
        object[] args = { parameterToPassIn };
        m.Invoke(null, BindingFlags.Default, binder, args, null);
        if (!binder.ChangeTypeCalled)
        {
        Console.WriteLine("Failed. Binder not given chance to fix things.");
        return;
        }
        Console.WriteLine("Passed.");
        return;
        }
        public static void Foo(void*pv)
        {
        }
        }
        public sealed unsafe class MyBinder : Binder
        {
        public bool ChangeTypeCalled { get; private set; }
        public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
        {
        throw new NotImplementedException();
        }
        public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
        {
        throw new NotImplementedException();
        }
        public sealed override object ChangeType(object value, Type type, CultureInfo culture)
        {
        ChangeTypeCalled = true;
        return Pointer.Box((void*)0, typeof(void*));
        }
        public sealed override void ReorderArgumentArray(ref object[] args, object state)
        {
        throw new NotImplementedException();
        }
        public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
        {
        throw new NotImplementedException();
        }
        public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
        {
        throw new NotImplementedException();
        }
        }
        }
        

        Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

        Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

        That is, on release builds.

        On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

        GC_TRIGGERS encountered in a GC_NOTRIGGER scope
        CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
        GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
        CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
        GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
        CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
        GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
        CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
        CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
        CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
        CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
        CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
        CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
        CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
        CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
        NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
        CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
        

        The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

        However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

         case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
        {
        // If we got the univeral zero...Then assign it and exit.
        if (rObj == 0) {
        *(PVOID *)pArgDst = 0;
        }
        else {
        if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
        else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
        ARG_SLOT slot;
        CreatePrimitiveValue(oType, oType, rObj, &slot);
        *(PVOID *)pArgDst = (PVOID)slot;
        }
        else
        COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
        }
        break;
        }
        

        Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

        Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

        Metadata

        Metadata

        Assignees

        Type

        No type

        Projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

          Description

          @ghost

          Consider the following test app:

          namespace ConsoleApp1
          {
          public unsafe class GG
          {
          public static void Main()
          {
          Test(UIntPtr.Zero);
          }
          public static void Test(object parameterToPassIn)
          {
          MyBinder binder = new MyBinder();
          MethodInfo m = typeof(GG).GetMethod("Foo");
          object[] args = { parameterToPassIn };
          m.Invoke(null, BindingFlags.Default, binder, args, null);
          if (!binder.ChangeTypeCalled)
          {
          Console.WriteLine("Failed. Binder not given chance to fix things.");
          return;
          }
          Console.WriteLine("Passed.");
          return;
          }
          public static void Foo(void*pv)
          {
          }
          }
          public sealed unsafe class MyBinder : Binder
          {
          public bool ChangeTypeCalled { get; private set; }
          public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
          {
          throw new NotImplementedException();
          }
          public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
          {
          throw new NotImplementedException();
          }
          public sealed override object ChangeType(object value, Type type, CultureInfo culture)
          {
          ChangeTypeCalled = true;
          return Pointer.Box((void*)0, typeof(void*));
          }
          public sealed override void ReorderArgumentArray(ref object[] args, object state)
          {
          throw new NotImplementedException();
          }
          public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
          {
          throw new NotImplementedException();
          }
          public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
          {
          throw new NotImplementedException();
          }
          }
          }
          

          Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

          Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

          That is, on release builds.

          On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

          GC_TRIGGERS encountered in a GC_NOTRIGGER scope
          CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
          GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
          CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
          GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
          CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
          GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
          CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
          CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
          CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
          CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
          CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
          CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
          CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
          CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
          NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
          CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
          

          The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

          However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

           case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
          {
          // If we got the univeral zero...Then assign it and exit.
          if (rObj == 0) {
          *(PVOID *)pArgDst = 0;
          }
          else {
          if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
          else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
          ARG_SLOT slot;
          CreatePrimitiveValue(oType, oType, rObj, &slot);
          *(PVOID *)pArgDst = (PVOID)slot;
          }
          else
          COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
          }
          break;
          }
          

          Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

          Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

          Metadata

          Metadata

          Assignees

          Type

          No type

          Projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

            Description

            @ghost

            Consider the following test app:

            namespace ConsoleApp1
            {
            public unsafe class GG
            {
            public static void Main()
            {
            Test(UIntPtr.Zero);
            }
            public static void Test(object parameterToPassIn)
            {
            MyBinder binder = new MyBinder();
            MethodInfo m = typeof(GG).GetMethod("Foo");
            object[] args = { parameterToPassIn };
            m.Invoke(null, BindingFlags.Default, binder, args, null);
            if (!binder.ChangeTypeCalled)
            {
            Console.WriteLine("Failed. Binder not given chance to fix things.");
            return;
            }
            Console.WriteLine("Passed.");
            return;
            }
            public static void Foo(void*pv)
            {
            }
            }
            public sealed unsafe class MyBinder : Binder
            {
            public bool ChangeTypeCalled { get; private set; }
            public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
            {
            throw new NotImplementedException();
            }
            public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
            {
            throw new NotImplementedException();
            }
            public sealed override object ChangeType(object value, Type type, CultureInfo culture)
            {
            ChangeTypeCalled = true;
            return Pointer.Box((void*)0, typeof(void*));
            }
            public sealed override void ReorderArgumentArray(ref object[] args, object state)
            {
            throw new NotImplementedException();
            }
            public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
            {
            throw new NotImplementedException();
            }
            public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
            {
            throw new NotImplementedException();
            }
            }
            }
            

            Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

            Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

            That is, on release builds.

            On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

            GC_TRIGGERS encountered in a GC_NOTRIGGER scope
            CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
            GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
            CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
            GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
            CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
            GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
            CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
            CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
            CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
            CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
            CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
            CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
            CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
            CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
            NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
            CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
            

            The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

            However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

             case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
            {
            // If we got the univeral zero...Then assign it and exit.
            if (rObj == 0) {
            *(PVOID *)pArgDst = 0;
            }
            else {
            if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
            else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
            ARG_SLOT slot;
            CreatePrimitiveValue(oType, oType, rObj, &slot);
            *(PVOID *)pArgDst = (PVOID)slot;
            }
            else
            COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
            }
            break;
            }
            

            Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

            Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

            Metadata

            Metadata

            Assignees

            Type

            No type

            Projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

              Description

              @ghost

              Consider the following test app:

              namespace ConsoleApp1
              {
              public unsafe class GG
              {
              public static void Main()
              {
              Test(UIntPtr.Zero);
              }
              public static void Test(object parameterToPassIn)
              {
              MyBinder binder = new MyBinder();
              MethodInfo m = typeof(GG).GetMethod("Foo");
              object[] args = { parameterToPassIn };
              m.Invoke(null, BindingFlags.Default, binder, args, null);
              if (!binder.ChangeTypeCalled)
              {
              Console.WriteLine("Failed. Binder not given chance to fix things.");
              return;
              }
              Console.WriteLine("Passed.");
              return;
              }
              public static void Foo(void*pv)
              {
              }
              }
              public sealed unsafe class MyBinder : Binder
              {
              public bool ChangeTypeCalled { get; private set; }
              public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
              {
              throw new NotImplementedException();
              }
              public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
              {
              throw new NotImplementedException();
              }
              public sealed override object ChangeType(object value, Type type, CultureInfo culture)
              {
              ChangeTypeCalled = true;
              return Pointer.Box((void*)0, typeof(void*));
              }
              public sealed override void ReorderArgumentArray(ref object[] args, object state)
              {
              throw new NotImplementedException();
              }
              public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
              {
              throw new NotImplementedException();
              }
              public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
              {
              throw new NotImplementedException();
              }
              }
              }
              

              Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

              Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

              That is, on release builds.

              On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

              GC_TRIGGERS encountered in a GC_NOTRIGGER scope
              CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
              GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
              CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
              GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
              CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
              GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
              CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
              CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
              CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
              CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
              CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
              CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
              CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
              CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
              NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
              CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
              

              The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

              However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

               case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
              {
              // If we got the univeral zero...Then assign it and exit.
              if (rObj == 0) {
              *(PVOID *)pArgDst = 0;
              }
              else {
              if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
              else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
              ARG_SLOT slot;
              CreatePrimitiveValue(oType, oType, rObj, &slot);
              *(PVOID *)pArgDst = (PVOID)slot;
              }
              else
              COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
              }
              break;
              }
              

              Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

              Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

              Metadata

              Metadata

              Assignees

              Type

              No type

              Projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                Type validation holes when using Reflection to invoke a method with a void* parameter. #7430

                Description

                @ghost

                Consider the following test app:

                namespace ConsoleApp1
                {
                public unsafe class GG
                {
                public static void Main()
                {
                Test(UIntPtr.Zero);
                }
                public static void Test(object parameterToPassIn)
                {
                MyBinder binder = new MyBinder();
                MethodInfo m = typeof(GG).GetMethod("Foo");
                object[] args = { parameterToPassIn };
                m.Invoke(null, BindingFlags.Default, binder, args, null);
                if (!binder.ChangeTypeCalled)
                {
                Console.WriteLine("Failed. Binder not given chance to fix things.");
                return;
                }
                Console.WriteLine("Passed.");
                return;
                }
                public static void Foo(void*pv)
                {
                }
                }
                public sealed unsafe class MyBinder : Binder
                {
                public bool ChangeTypeCalled { get; private set; }
                public sealed override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture)
                {
                throw new NotImplementedException();
                }
                public sealed override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state)
                {
                throw new NotImplementedException();
                }
                public sealed override object ChangeType(object value, Type type, CultureInfo culture)
                {
                ChangeTypeCalled = true;
                return Pointer.Box((void*)0, typeof(void*));
                }
                public sealed override void ReorderArgumentArray(ref object[] args, object state)
                {
                throw new NotImplementedException();
                }
                public sealed override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
                {
                throw new NotImplementedException();
                }
                public sealed override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
                {
                throw new NotImplementedException();
                }
                }
                }
                

                Passing a UIntPtr to a void* isn't allowed so what should happen is that Reflection should call MyBinder.ChangeType() to give it a chance to convert the UIntPtr into something that is acceptable.

                Instead, Reflection throws an ArgumentException with the message "Object type cannot be converted to target type." and never calls the binder.

                That is, on release builds.

                On checked builds, it triggers a GC_NOTRIGGER contract violation while propagating the exception:

                GC_TRIGGERS encountered in a GC_NOTRIGGER scope
                CONTRACT in CLRException::GetThrowableFromException at "c:\dd\coreclr\src\vm\clrex.cpp" @ 714
                GCX_COOP in UnwindAndContinueRethrowHelperInsideCatch at "c:\dd\coreclr\src\vm\excep.cpp" @ 8566
                CONTRACT in MethodDescCallSite::CallTargetWorker at "c:\dd\coreclr\src\vm\callhelpers.cpp" @ 361
                GCX_COOP in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2468
                CONTRACT in Assembly::ExecuteMainMethod at "c:\dd\coreclr\src\vm\assembly.cpp" @ 2452
                GCX_COOP in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 651
                CONTRACT in CorHost2::ExecuteAssembly at "c:\dd\coreclr\src\vm\corhost.cpp" @ 595
                CORECLR! CONTRACT_ASSERT + 0x342 (0x00007ffa`f693acf2)
                CORECLR! EEContract::DoChecks + 0x3DE (0x00007ffa`f6ad1b8e)
                CORECLR! CLRException::GetThrowableFromException + 0x14B (0x00007ffa`f6b164cb)
                CORECLR! UnwindAndContinueRethrowHelperInsideCatch + 0xBB (0x00007ffa`f6acf6fb)
                CORECLR! `RuntimeMethodHandle::InvokeMethod'::`1'::catch$11 + 0xF4 (0x00007ffa`f78c308f)
                CORECLR! CallSettingFrame + 0x20 (0x00007ffa`f7611990)
                CORECLR! _CxxCallCatchBlock + 0x15A (0x00007ffa`f760c16a)
                NTDLL! RtlCaptureContext + 0x3C3 (0x00007ffb`442ea193)
                CORECLR! RuntimeMethodHandle::InvokeMethod + 0xDEE (0x00007ffa`f72e246e)
                

                The problems are multifold. First, when the formal parameter type is a pointer to void, the code that's supposed to catch invalid arguments and divert to the binder (https://github.com/dotnet/coreclr/blob/master/src/vm/reflectioninvocation.cpp#L298) abdicates from the duty and declares virtually any input argument as legal. So the binder never gets called and Reflection proceeds to execute the call.

                However, the calling mechanism performs its own type checks so which happens at InvokeUtil::CopyArg() (https://github.com/dotnet/coreclr/blob/master/src/vm/invokeutil.cpp#L299)

                 case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_FNPTR:
                {
                // If we got the univeral zero...Then assign it and exit.
                if (rObj == 0) {
                *(PVOID *)pArgDst = 0;
                }
                else {
                if (rObj->GetMethodTable() == MscorlibBinder::GetClassIfExist(CLASS__POINTER) && type == ELEMENT_TYPE_PTR) *(PVOID *)pArgDst = GetPointerValue(rObj);
                else if (rObj->GetTypeHandle().AsMethodTable() == MscorlibBinder::GetElementType(ELEMENT_TYPE_I)) {
                ARG_SLOT slot;
                CreatePrimitiveValue(oType, oType, rObj, &slot);
                *(PVOID *)pArgDst = (PVOID)slot;
                }
                else
                COMPlusThrow(kArgumentException,W("Arg_ObjObj"));
                }
                break;
                }
                

                Firstly, there's the unguarded call to "AsMethodTable()" for the purpose of seeing if "rObj" is a System.IntPtr. If you pass in something like an int[], this triggers an !IsTypeDesc() assertion inside AsMethodTable. Since the returned "pointer" value is only compared to another constant pointer value, there's no major risk here but it is shabby.

                Two, the COMPlusThrow() at this point triggers the aforementioned GC_NOTRIGGER violation while bubbling up.

                Metadata

                Metadata

                Assignees

                Type

                No type

                Projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions