Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BadImageFormatException on startup after adding a nested type to <Module> in .NET 9 #111164

Closed
4nonym0us opened this issue Jan 7, 2025 · 5 comments · Fixed by #111435
Closed
Assignees
Labels
area-TypeSystem-coreclr in-pr There is an active PR which will close this issue when it is merged regression-from-last-release

Comments

@4nonym0us
Copy link

Description

Adding a nested type to the <Module> using dnlib/Cecil/AsmResolver used to be supported until .NET 9. Currently, [.NET 9] applications that have a nested type in the <Module>, would just crash on startup due to unhandled BadImageFormatException:

Unhandled exception. System.BadImageFormatException: Enclosing type(s) not found for type 'NestedType' in assembly 'BlankConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

Utilizing <Module> is not that uncommon when it's necessary to deprotect/unpack embedded resources or decrypt constants (or string literals) in runtime to make reverse-engineering of the assembly not as trivial as it is.

This regression made some .NET obfuscators/protectors incompatible with .NET 9. This breaking change not being documented in the list of .NET 9 breaking changes (please, correct me if I'm wrong) makes it unclear whether the regression is a bug or merely a result of a well-thought design decision (which could be understandable granted that nested types in a global module must be very rare).

Reproduction Steps

  1. Create a blank .NET 9 project from the Console App template.
  2. Run dotnet publish -r win-x64 -f net9.0 to produce an assembly.
  3. Use dnlib, Mono.Cecil, AsmResolver, or any other tool to inject a nested type into the <Module> (alternatively, it's possible to manually add the nested type via dnSpy). Sample code for Mono.Cecil (dotnet add package Mono.Cecil --version 0.11.6):
const string asmPath = "BlankConsoleApp.dll";

var assembly = Mono.Cecil.AssemblyDefinition.ReadAssembly(asmPath, new ReaderParameters { ReadWrite = true });

var nestedType = new Mono.Cecil.TypeDefinition(
    null,
    "NestedType",
    TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed,
    assembly.MainModule.ImportReference(typeof(System.ValueType)));

var mainModule = assembly.MainModule.GetTypes().Single(t => t.FullName == "<Module>");
mainModule.NestedTypes.Add(nestedType);

assembly.Write(new Mono.Cecil.WriterParameters
{
    WriteSymbols = true,
    SymbolWriterProvider = new Mono.Cecil.Cil.PortablePdbWriterProvider()
});

where asmPath is a path to a .NET assembly that needs to be processed, obtained in a previous step.

Note that this is not a Mono.Cecil bug, I have also tested all of the other libraries mentioned above and tried adding the nested type manually using dnSpy, the issue persisted.

  1. Run the application, an unhandled exception is thrown:

Unhandled exception. System.BadImageFormatException: Enclosing type(s) not found for type 'NestedType' in assembly 'BlankConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

Repeating the same steps when targeting .NET 8 (or earlier versions) or .NET Framework would produce a valid assembly, which wouldn't crash on startup.


I'm attaching final binaries for .NET 8 and .NET 9, which were created using the steps, described above.
BlankConsoleApp_NestedType_net8.zip ← working .NET 8 application with a NestedType in a <Module>.
BlankConsoleApp_NestedType_net9.zip ← the same application, which targets .NET 9 and crashes on startup.

Expected behavior

Application shouldn't crash, enclosing type should be resolved (even if it's the <Module> class) as long as all of the necessary TypeDef and NestedClass entries are present in the corresponding CLR metadata tables.

On the other hand, if the breaking change was introduced by design, it should be documented on the Breaking changes in .NET 9 to prevent confusion.

Actual behavior

Following the reproduction steps above leads to .NET 9 assembly being corrupted, which is a regression from .NET 8.

Exception is thrown on startup:

Unhandled exception. System.BadImageFormatException: Enclosing type(s) not found for type 'NestedType' in assembly 'BlankConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

Regression?

It is a regression, nested types in the <Module> are still supported on .NET Framework and has been supported on .NET Core until .NET 9 (.NET 8 is the latest version that works correctly).

Known Workarounds

I guess not using nested types in the <Module>. It's still possible to create nested types, it's just that the enclosing type must not be <Module> anymore.

Configuration

SDK: .NET SDK 9.0.101
Runtime: win-x64

I'm quite confident that the issue is not platform-specific.

Other information

The IL of the <Module> with a nested struct is completely identical in .NET 8 and .NET 9:

.class private auto ansi '<Module>'
{
	// Nested Types
	.class nested private explicit ansi sealed NestedType
		extends [System.Runtime]System.ValueType
	{
	} // end of class NestedType


} // end of class <Module>

All of the CLR metadata tables are exactly the same, both assemblies (.NET 8 and .NET 9) have a TypeDef for a NestedType, <Module> and BlankConsoleApp.Program, as well as NestedClass definition for NestedType having <Module> as it's enclosing type.

The exception is being thrown on clsload.cpp#3487:

pModule->GetAssembly()->ThrowBadImageException(pszNameSpace, pszName, BFA_ENCLOSING_TYPE_NOT_FOUND);

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jan 7, 2025
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Jan 7, 2025
@vcsjones vcsjones added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jan 7, 2025
Copy link
Contributor

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented Jan 7, 2025

Likely introduced by #94825

cc @davidwrighton

@davidwrighton davidwrighton self-assigned this Jan 8, 2025
davidwrighton added a commit to davidwrighton/runtime that referenced this issue Jan 14, 2025
- This isn't disallowed by spec, although ilasm and ildasm cannot handle these cases
- Simply skip adding the type to the available class hash.

Fix dotnet#111164
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Jan 14, 2025
@dotnet-policy-service dotnet-policy-service bot removed the untriaged New issue has not been triaged by the area owner label Jan 16, 2025
davidwrighton added a commit to davidwrighton/runtime that referenced this issue Jan 16, 2025
- This isn't disallowed by spec, although ilasm and ildasm cannot handle these cases
- Simply skip adding the type to the available class hash.

Backport of bugfix portion of PR dotnet#111435
Fixes dotnet#111164 in .NET 9
@davidwrighton
Copy link
Member

@4nonym0us the backport of this fix to .NET 9 was rejected with the following comment. #111479 (comment) If you have additional feedback to provide, please either re-open this issue or file a new one with evidence of the impact that not fixing this will have.

@4nonym0us
Copy link
Author

@davidwrighton no problem. Even though I would like to have it backported to .NET 9, I tend to agree with the reviewer of the backport PR about this regression not having a widespread impact (the regression being spotted so late is a fine indicator of that).

Personally, I can't even come up with any other use case of this other than assembly protection. There are not that many scenarios where code obfuscation makes sense and there are very few [decent] obfuscation tools as a result of that, which greatly narrows the scope of possibly affected projects. Furthermore, some other tools that I know of just inject their custom types into a different (not <Module>) base type already, so they were not affected.

@GruberMarkus
Copy link

@4nonym0us Thanks for reporting this issue! You mention assembly protection tools. Would you mind sharing which ones already have a workaround for this issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-TypeSystem-coreclr in-pr There is an active PR which will close this issue when it is merged regression-from-last-release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants