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)
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### 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 ` (COR_GLOBAL_PARENT_TOKEN)`:
```chsarp
if (SUCCEEDED(pMDImport->GetNestedClassProps(classdef, &enclosing))) {
// nested type
if (enclosing == COR_GLOBAL_PARENT_TOKEN)
{
// Types nested in the 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 `` (i.e. ` → 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-``) 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:
```csharp
using System;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
// mode: "flat" = -> TypeA ; "deep" = -> 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(""), 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 `` 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_
Contributor guide
Research direction
Start with the supplied MetadataBuilder reproducer and loader, comparing flat and deep assemblies on net10.0. Read src/coreclr/vm/clsload.cpp, especially ClassLoader::AddAvailableClassHaveLock, and trace nested-type handling for the → TypeA → TypeB case. Done means the deep assembly loads successfully without BadImageFormatException while preserving the direct-child behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- infrastructure
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100