1

I'm writing a compiler that outputs .NET assemblies (using Mono.Cecil, although I don't believe Cecil is relevant to this problem). One of the compiler features requires that a class will have a compiler-generated nested class with some support methods; the outer class has a static field so each class effectively has a singleton referencing an object of the nested class. To initialise this, any such class has a class constructor to create an instance of the nested class and store it in the field.

Problem: When my outer class is a generic class, I make the nested class generic too (since it needs to create objects of the outer class). The generated IL passes through peverify fine, and looks fine to my eye, but the class constructor creating an instance of the nested class throws OutOfMemoryException at runtime.

I've disassembled the assembly with ildasm, slimmed it down to a minimal reproduction (unfortunately still ~180 lines of IL), and verifying that compiling the IL with ilasm produces an exe that still exhibits the problem.

Debugging with Visual Studio or MDbg hasn't enlightened me - I just get OutOfMemoryException without indication why. I'm willing to believe my IL is invalid somehow, but peverify doesn't indicate a problem. Can anyone suggest what the issue is?

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly extern System
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly Repro1
{
  .ver 0:0:0:0
}
.module Repro1
// MVID: {7DA983B6-F5EA-4ACB-8443-C29F25ADDCD4}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x016E0000

.class public abstract auto ansi sealed Repro1
       extends [mscorlib]System.Object
{
  .method assembly static void  '<NemeaProgram>'() cil managed
  {
    .entrypoint
    // Code size       6 (0x6)
    .maxstack  0
    IL_0000:  call       void Rep2::Go()
    IL_0005:  ret
  } // end of method Repro1::'<NemeaProgram>'

} // end of class Repro1

.class public abstract auto ansi sealed Rep1
       extends [mscorlib]System.Object
{
  .class auto ansi nested public TRep
         extends [mscorlib]System.Object
  {
    .class auto ansi nested public '__%NemeaVType'
           extends [mscorlib]System.Object
    {
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // Code size       7 (0x7)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
        IL_0006:  ret
      } // end of method '__%NemeaVType'::.ctor

      .method public newslot virtual instance class Rep1/TRep 
              Create(string Foo) cil managed
      {
        // Code size       10 (0xa)
        .maxstack  8
        IL_0000:  ldarg      Foo
        IL_0004:  newobj     instance void Rep1/TRep::.ctor(string)
        IL_0009:  ret
      } // end of method '__%NemeaVType'::Create

    } // end of class '__%NemeaVType'

    .field famorassem string FData
    .field public static class Rep1/TRep/'__%NemeaVType' '__%NemeaVTypeI'

    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(string Foo) cil managed
    {
      // Code size       17 (0x11)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
      IL_0006:  ldarg.0
      IL_0007:  ldarg      Foo
      IL_000b:  stfld      string Rep1/TRep::FData
      IL_0010:  ret
    } // end of method TRep::.ctor

    .method privatescope specialname rtspecialname static 
            void  '.cctor$PST0600004C'() cil managed
    {
      // Code size       11 (0xb)
      .maxstack  8
      IL_0000:  newobj     instance void Rep1/TRep/'__%NemeaVType'::.ctor()
      IL_0005:  stsfld     class Rep1/TRep/'__%NemeaVType' Rep1/TRep::'__%NemeaVTypeI'
      IL_000a:  ret
    } // end of method TRep::.cctor

  } // end of class TRep

  .class auto ansi nested public TItem
         extends [mscorlib]System.Object
  {
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor() cil managed
    {
      // Code size       7 (0x7)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
      IL_0006:  ret
    } // end of method TItem::.ctor

  } // end of class TItem

} // end of class Rep1

.class public abstract auto ansi sealed Rep2
       extends [mscorlib]System.Object
{
  .class auto ansi nested public TRep<(Rep1/TItem) T>
         extends Rep1/TRep
  {
    .class auto ansi nested public '__%NemeaVType'<(Rep1/TItem) T_vt>
           extends Rep1/TRep/'__%NemeaVType'
    {
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // Code size       7 (0x7)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void Rep1/TRep/'__%NemeaVType'::.ctor()
        IL_0006:  ret
      } // end of method '__%NemeaVType'::.ctor

      .method public virtual instance class Rep1/TRep 
              Create(string Foo) cil managed
      {
        // Code size       10 (0xa)
        .maxstack  8
        IL_0000:  ldarg      Foo
        IL_0004:  newobj     instance void class Rep2/TRep<!T_vt>::.ctor(string)
        IL_0009:  ret
      } // end of method '__%NemeaVType'::Create

    } // end of class '__%NemeaVType'

    .field public static class Rep2/TRep/'__%NemeaVType'<!T> '__%NemeaVTypeI'
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(string Foo) cil managed
    {
      // Code size       22 (0x16)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  ldarg      Foo
      IL_0005:  call       instance void Rep1/TRep::.ctor(string)
      IL_0015:  ret
    } // end of method TRep::.ctor

    .method privatescope specialname rtspecialname static 
            void  '.cctor$PST06000055'() cil managed
    {
      // Code size       11 (0xb)
      .maxstack  8
      IL_0000:  newobj     instance void class Rep2/TRep/'__%NemeaVType'<!T>::.ctor()
      IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!T> Rep2/TRep::'__%NemeaVTypeI'
      IL_000a:  ret
    } // end of method TRep::.cctor

  } // end of class TRep

  .method public static void  Go() cil managed
  {
    // Code size       29 (0x1d)
    .maxstack  1
    .locals init ([0] class Rep1/TRep R)
    IL_0000:  ldstr      "Oi"
    IL_0005:  newobj     instance void class Rep2/TRep<class Rep1/TItem>::.ctor(string)
    IL_000a:  stloc      R
    IL_001c:  ret
  } // end of method Rep2::Go

} // end of class Rep2
DJC_ksd
  • 31
  • 4
  • 1
    Well, the thing I do in a case like this, is to create the equivalent C# code, compile it, decompile back to MSIL and see what's the difference :) Of course, you might already find the bug when writing the equivalent C# code, but that's just bonus points. – Luaan Aug 23 '16 at 09:10
  • However, in this case, I think the problem is that the nested class should have the same type argument as the parent class. You're declaring a new type argument `T_vt` without using the parent's `T`, which is definitely not what you want, and likely an error that might be causing the exception you're seeing (since your nested class *must* use all the generic type arguments of its parent class). – Luaan Aug 23 '16 at 09:16
  • No, I *think* what I'm doing with the nested class type arguments is correct. It's declared with its own type argument T_vt, and when the outer class creates an instance of it, it does so using "newobj instance void class Rep2/TRep/'__%NemeaVType'<!T>::.ctor()" - i.e. it passes it's own type argument <!T> in as the type argument to the nested class. So the nested class will be instantiated using the same actual type argument. I will try replicating in C#, although some of the things I'm doing, I'm not sure how they'll map into C#. – DJC_ksd Aug 23 '16 at 10:20
  • ...although it turns out you're quite right, the nested class *does* need to use the parent type argument directly. I suspect this is the answer (although it's annoying that peverify doesn't highlight it!) - if I confirm that, I will post an answer accordingly. Thanks. – DJC_ksd Aug 23 '16 at 10:41
  • Well, AFAIK, PEVerify is only supposed to ensure that the IL code is managed-safe - that is, it doesn't allow partially trusted code to do unsafe things (this mostly means preventing issues like stack disbalancing, type-safety violation etc,). It's not really designed to ensure the code is correct - that's not possible even in theory. It would be nice if it could call more of the common errors, but that's not really the purpose of the tool. Maybe there are some tools that would help you there, but I don't know of any :) – Luaan Aug 23 '16 at 11:17

1 Answers1

2

So it turns out the problem isn't anything to do with the generic declarations - the declaration of the nested class is fine. The name of the generic parameter doesn't match the generic parameter of the outer class, but this is just a convention that the C# compiler (understandably) adheres to when it's propagating generic parameters to nested classes.

The problem is simply the

 IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!T> Rep2/TRep::'__%NemeaVTypeI'

line - it's not valid, because it's trying to access a field on the Rep2/TRep class, which is generic, without providing any type arguments. Changing this to

 IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!0> Rep2/TRep<!T>::'__%NemeaVTypeI'

resolves all the issues.

I still hold that peverify could have highlighted this, because it's not really valid - it couldn't possibly execute correctly since it's not a valid field reference. It's also a little annoying that you get an OutOfMemoryException when executing the code, rather than anything with more details.

DJC_ksd
  • 31
  • 4