We made a change in Whidbey recently that impacts the verification of call
s to virtual methods.
Valid IL could previously invoke a precise implementation of a virtual method
with a call
instruction instead of a callvirt
. The target type’s exact
method token could be specified, bypassing all dynamic dispatch altogether. For
example, given two classes A
and B
class A
{
public virtual void f()
{
Console.WriteLine("A::f");
}
}
class B : A
{
public override void f()
{
Console.WriteLine("B::f");
}
}
a consumer would ordinarily emit IL to perform a virtual dispatch, looking something like this in IL
newobj instance void B::.ctor()
callvirt instance void A::f()
The result is of course a properly dispatched virtual call which resolves to
B
s override and prints out “B::f”. But somebody could do this instead
newobj instance void B::.ctor()
call instance void A::f()
The result of which is an ordinary statically dispatched call to A
s
implementation of f, printing out “A::f”.
Some consider this a violation of privacy through inheritence. Lots of code
is written under the assumption that overriding a virtual method is sufficient
to guarantee custom logic within gets called. Intuitively, this makes sense,
and C# lulls you into this sense of security because it always emits calls to
virtual methods as callvirt
s. C++ offers language syntax to do precisely
this, however, e.g.
B b;
b.A::f();
I don’t know of any other language that support this type of call directly, but presumably somebody else followed in C++’s footsteps here. C# (and others) use this technique to implement ‘call to base’ functionality. Some compilers emit this type of IL so that their method resolution code can bind to virtual methods in a custom way. And others could do it in an attempt to “devirtualize” method calls when they know there are no overrides.
Late in Whidbey, some folks decided this is subtly strange enough that we at least don’t want partially trusted code to be doing it. That it’s even possible is often surprising to people. We resolved the mismatch between expectations and reality through the introduction of a new verification rule.
The rule restricts the manner in which callers can make non-virtual calls to
virtual methods, specifically by only permitting it if the target method is
being called on the caller’s this
pointer. This effectively allows an object
to call up (or down, although that would be odd) its own type hierarchy. With
this change, the above example fails verification, “The ‘this’ parameter to the
call must be the calling method’s ‘this’ parameter.”
The verifier implements this magic using a technique called identity tracking.
We don’t use this style of tracking in many places. The verifier ordinarily
tracks only the static type of items on the stack. But in this case, it needs
to be comfortable that you’re using the same arg.0
pointer for the method
call as was passed onto the caller’s stack frame. If you’ve executed a starg.0
in the IL stream, for example, you won’t be permitted to make the call. Even
if you do a ldarg.0
followed by a starg 0
, the verifier tosses you out the
window.
A catch here is that while you might be operating dynamically on the this
pointer, the verifier avoids statically tracking pointers across method calls.
An example of where this can produce a false positive is as follows
class A
{
public virtual void f()
{
Console.WriteLine("Foo::f");
}
}
class B : A
{
public override void f()
{
Console.WriteLine("Bar::f");
}
private B Echo(B b)
{
return b;
}
public void FailsVerification()
{
Echo(this).A::f();
}
}
It’s clear that FailsVerification
is really just invoking methods on its this
pointer. But it does so in a roundabout fashion. (Of course that A::f()
syntax is psuedo-code; it would compile in C++, but C# doesn’t offer such a
feature.) Regardless, the IL that gets produced isn’t verifiable.