cdhowie
cdhowie

Reputation: 169008

CIL delegate behavior with conflicting "staticness" of target method

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?

Upvotes: 5

Views: 420

Answers (3)

Carlo Kok
Carlo Kok

Reputation: 1168

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:

  • Without this, in which case NULL should be passed
  • With the extra first parameter, presuming the type matches what was fed into the newobj call.

Upvotes: 5

Scott Wisniewski
Scott Wisniewski

Reputation: 25041

It's worth pointing out that this isn't an abuse. It's a technique known as "delegate currying". It's comes from a more general technique called "currying" in functional programming languages, where a function with N+1 arguments is converted into a function with N arguments.The C# equivalent would look something like:

Func<T2, R> CurryFirst<T1, T2, R>(
    Func<T1, T2, R> f,
    T1 arg
)
{
    return (x) => f(arg, x);
}

Func<T1, R> CurrySecond<T1, T2, R>(
    Func<T1, T2, R> f,
    T2 arg
)
{
    return (x) => f(x, arg);
}

The CLR provides special support for the "curry first" case, mainly because at the machine code level, a curried static method invocation looks almost exactly like an instance method invocation (the this parameter is passed in as the implicit first argument).

That makes implementing delegate currying fairly efficient. It was originally implemented, along with DynamicMethod to support Iron Python. It's also used for other purposes, such as allowing delegates to refer to extension methods transparently.

Upvotes: 3

cdhowie
cdhowie

Reputation: 169008

Well, I didn't think I'd be answering my first question myself...

A colleague in #mono (Ck) has informed me of the relevant behavior of Delegate.CreateDelegate: (emphasis mine)

If firstArgument is supplied, it is passed to method every time the delegate is invoked; firstArgument is said to be bound to the delegate, and the delegate is said to be closed over its first argument. If method is static (Shared in Visual Basic), the argument list supplied when invoking the delegate includes all parameters except the first; if method is an instance method, then firstArgument is passed to the hidden instance parameter (represented by this in C#, or by Me in Visual Basic).

It seems logical to me to conclude from this documentation, that this (ab)use of ldftn during delegate construction, combined with a non-null target, is actually well-defined behavior.

Upvotes: 1

Related Questions