dotnet / dotnet/runtime

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)

Open
#132,551 2 comments 0 reactions 0 assignees View on GitHub
area-TypeSystem-coreclr untriaged
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.