This question will take a bit of introduction.
I am working on a security project that will analyze CIL assemblies and reject those that do certain defined "bad" things while also allowing the hosting application to supply "gates" for some methods, to allow some calls to be filtered. (This is a small subset of the functionality of the project, but it's the part I'll be inquiring about here.)
The project scans all of the instructions in every method in the assembly, and looks for the call, callvirt, ldftn, ldvirtftn, and newobj opcodes, since these are the only opcodes that can ultimately result in a method call. The ldftn opcodes are used when constructing delegates, like so:
ldarg.1
ldftn instance bool string::EndsWith(string)
newobj instance void class [System.Core]System.Func`2<string, bool>::'.ctor'(object, native int)
At the end of this sequence, a Func<string, bool>
is on the top of the stack.
Let's say that I want to intercept all calls to String.EndsWith(String)
. For call and callvirt, I can just replace the instance call with a static call of the signature Boolean(String,String)
-- the first argument will be the string instance the method was originally invoked on. On a CIL level the behavior will be unambiguous and well-defined, since this is how static methods are called.
But for ldftn? I tried just replacing the operand of the ldftn instruction with the same static method used to replace call/callvirt's operand:
ldarg.1
ldftn bool class Prototype.Program::EndsWithGate(string, string)
newobj instance void class [System.Core]System.Func`2<string, bool>::'.ctor'(object, native int)
I was fully expecting this to fail, since the delegate is given a target object (not null) while handed a static method pointer. To my surprise, this actually works on both the Microsoft .NET runtime and Mono. I understand that the target/this parameter is just the first parameter to the method, and is hidden for instance methods. (The project is based on this knowledge.) But the fact that delegates actually work under these circumstances is a bit puzzling to me.
So, my question: is this defined and documented behavior? Will delegates, when invoked, always push their target onto the stack if it's not null? Would it be better to construct a closure class that will capture the target and "properly" call the static method, even though this will be a lot more complex and annoying?
ECMA 335 spec part 2 14.6.2 has a paragraph about this: The calling convention of T and D shall match exactly, ignoring the distinction between static and instance methods. (i.e. the this parameter if any, is not treated specially).
What this sounds to me like that for static methods will be allowed in two variations:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With