Skip to content

Generation of dynamic types with multiple threads using System.Reflection.Emit components causes runtime corruption #64094

Open
@ryanovic

Description

@ryanovic

Description

TypeBuilder could throw a random exception when multiple instances, that are defined in the same ModuleBuilder, are generated in different threads. It seems like a dynamic module itself got corrupted. It happens on a fresh instance only, meaning in the code sample below, increasing a number of iteration doesn't increase probability of an error, if the root module is not re-initialized at the beginning.

Reproduction Steps

using System;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading.Tasks;

public class TestBase
{
    public readonly TextReader reader;

    public TestBase(TextReader reader)
    {
        this.reader = reader;
    }
}

public interface ITest
{
    string Test(int num);
}

public class Program
{
    private static object syncroot = new object();
    private const int N = 10;

    public static void Main()
    {
        try
        {
            for (int step = 0; ; step++)
            {
                var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("test" + step.ToString()), AssemblyBuilderAccess.Run);
                var module = assembly.DefineDynamicModule("test" + step++.ToString());
                var types = new Type[N];

                Console.Write(step.ToString() + '\r');

                Parallel.For(0, N, (i) =>
                {
                    TypeBuilder tb;

                    lock (syncroot) 
                    {
                        tb = module.DefineType("test_" + i.ToString(), TypeAttributes.Public, typeof(TestBase), new[] { typeof(ITest) });
                    }

                    MethodBuilder mb;

                    // The following lock sections commented are enough to workaroud the issue.
                    // Basically an error occurs when a new assembly reference is internally added in the assembly builder.
                    // Note, each thread is working with its own TypeBuilder, no shared  context, except the initial DefineType call.
                    
                    //lock (syncroot)
                    {
                        mb = tb.DefineMethod("Test", MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), new[] { typeof(int) });
                    }

                    var il = mb.GetILGenerator();
                    var label = il.DefineLabel();

                    for (int j = 0; j < 50; j++)
                    {
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldc_I4, j);
                        il.Emit(OpCodes.Bne_Un, label);

                        il.Emit(OpCodes.Ldstr, j.ToString());
                        il.Emit(OpCodes.Ret);

                        il.MarkLabel(label);
                        label = il.DefineLabel();
                    }

                    il.MarkLabel(label);
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Ret);

                    ConstructorBuilder cb;

                    //lock (syncroot)
                    {
                        cb = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(TextReader) });
                    }

                    il = cb.GetILGenerator();
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);

                    //lock (syncroot)
                    {
                        il.Emit(OpCodes.Call, typeof(TestBase).GetConstructor(new[] { typeof(TextReader) }));
                    }

                    il.Emit(OpCodes.Ret);

                    types[i] = tb.CreateType();

                });
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }
}

Expected behavior

TypeBuilder instances manage their internal shared context in a thread-safe manner.

Actual behavior

Examples of exception types could be thrown, though, seems, their just indicates a corrupted state :

System.BadImageFormatException: An attempt was made to load a program with an incorrect format. (0x8007000B)
at System.Reflection.Emit.ModuleBuilder.GetMemberRefOfMethodInfo(QCallModule module, Int32 tr, RuntimeMethodHandleInternal method)

Fatal error. Internal CLR error. (0x80131506)
at System.Reflection.Emit.TypeBuilder.DefineType(System.Runtime.CompilerServices.QCallModule, System.String, Int32, System.Reflection.TypeAttributes, Int32, Int32[])

System.TypeLoadException: The signature is incorrect.
at System.Reflection.Emit.TypeBuilder.CreateTypeNoLock()

Regression?

No response

Known Workarounds

No response

Configuration

Reproduced with .Net Core 3.1, .Net 5, .Net 6
Windows 10, x64

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Reflection.EmitenhancementProduct code improvement that does NOT require public API changes/additionshelp wanted[up-for-grabs] Good issue for external contributors

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions