Skip to content

BadImageFormatException: Enclosing type(s) not found still occurs when a type is nested two or more levels deep under <Module> (fix #111435 only covers the direct-child case) #132551

Description

@OlegFM

Description

#111164 reported that types nested directly inside fail to load on .NET 9+ with BadImageFormatException: Enclosing type(s) not found for type 'X' in assembly 'Y'.

This was fixed by #111435, which adds an early return in ClassLoader::AddAvailableClassHaveLock (src/coreclr/vm/clsload.cpp) whenever a type's own enclosing type is <Module> (COR_GLOBAL_PARENT_TOKEN):

if (SUCCEEDED(pMDImport->GetNestedClassProps(classdef, &enclosing))) {
    // nested type
    if (enclosing == COR_GLOBAL_PARENT_TOKEN)
    {
        // Types nested in the <module> class can't be found by lookup.
        return;
    }
    ...

That fix skips registering the directly-nested type in the available-class hash, which avoids the crash for that one type. However, if some other type B is nested inside a type A that is itself nested inside <Module> (i.e. <Module> → A → B, two levels deep), loading still throws the same BadImageFormatException, now naming B instead of A. This is presumably because when B's entry is processed, the loader looks up its immediate enclosing type A in the same hash table — but A was deliberately never added to that table by the #111435 fix, so the lookup misses and the original "enclosing type not found" failure path fires one level down.

This pattern (helper types injected as nested-in-nested-in-<Module>) is common in .NET obfuscators/protectors (ConfuserEx and derivatives) that clone runtime-support types into the target module for constant decryption / anti-debug checks, so it's the same class of regression as #111164, just one level deeper and apparently missed by the original fix and its repro.

Reproduction Steps

Minimal repro, built with System.Reflection.Metadata.Ecma335.MetadataBuilder directly (no obfuscator involved), targeting net10.0:

using System;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;

// mode: "flat" = <Module> -> TypeA ; "deep" = <Module> -> TypeA -> TypeB
string mode = args.Length > 0 ? args[0] : "flat";
string outPath = args.Length > 1 ? args[1] : "out.dll";

var metadata = new MetadataBuilder();

metadata.AddModule(0, metadata.GetOrAddString("ReproModule"),
    metadata.GetOrAddGuid(Guid.NewGuid()), default, default);

metadata.AddAssembly(metadata.GetOrAddString("ReproAsm"), new Version(1, 0, 0, 0),
    default, default, AssemblyFlags.PublicKey, AssemblyHashAlgorithm.None);

var systemRuntimeRef = metadata.AddAssemblyReference(
    metadata.GetOrAddString("System.Runtime"), new Version(10, 0, 0, 0), default,
    metadata.GetOrAddBlob(new byte[] { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a }),
    default, default);

var objectTypeRef = metadata.AddTypeReference(systemRuntimeRef,
    metadata.GetOrAddString("System"), metadata.GetOrAddString("Object"));

var moduleType = metadata.AddTypeDefinition(default, default,
    metadata.GetOrAddString("<Module>"), default,
    MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1));

var typeA = metadata.AddTypeDefinition(
    TypeAttributes.NestedAssembly | TypeAttributes.Sealed, default,
    metadata.GetOrAddString("TypeA"), objectTypeRef,
    MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1));
metadata.AddNestedType(typeA, moduleType);

if (mode == "deep")
{
    var typeB = metadata.AddTypeDefinition(
        TypeAttributes.NestedAssembly | TypeAttributes.Sealed, default,
        metadata.GetOrAddString("TypeB"), objectTypeRef,
        MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1));
    metadata.AddNestedType(typeB, typeA);
}

var peBuilder = new ManagedPEBuilder(
    new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll),
    new MetadataRootBuilder(metadata), new BlobBuilder(),
    entryPoint: default, flags: CorFlags.ILOnly);

var peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);
using var fsOut = new FileStream(outPath, FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fsOut);

Loader (also net10.0):

using System;
using System.IO;
using System.Reflection;

var bytes = File.ReadAllBytes(args[0]);
try
{
    var asm = Assembly.Load(bytes);
    Console.WriteLine("LOAD OK: " + asm.FullName);
    Console.WriteLine("GetTypes OK, count=" + asm.GetTypes().Length);
}
catch (Exception ex)
{
    Console.WriteLine("LOAD FAILED: " + ex.GetType().Name + ": " + ex.Message);
}

Steps:

  1. Generate repro_flat.dll (mode flat) and repro_deep.dll (mode deep).
  2. Run the loader against each with a net10.0 app.

Expected behavior

Both repro_flat.dll and repro_deep.dll load successfully, consistent with the intent of #111435 that types nested under <Module> are tolerated (even if not discoverable by name lookup).

Actual behavior

$ loader repro_flat.dll
LOAD OK: ReproAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
GetTypes OK, count=1
$ loader repro_deep.dll
LOAD FAILED: BadImageFormatException: Enclosing type(s) not found for type 'TypeB' in assembly 'ReproAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

repro_deep.dll fails at Assembly.Load itself, before any type is even requested.

Regression?

Runtime flat (1 level) deep (2 levels)
.NET 6.0.36 loads, GetTypes() throws ReflectionTypeLoadException loads, GetTypes() throws ReflectionTypeLoadException
.NET 8.0.30 loads fine loads fine
.NET 10.0.11 (SDK 10.0.111) loads fine (fixed by #111435) Assembly.Load throws BadImageFormatException

Known Workarounds

No response

Configuration

No response

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions