dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.28k stars 4.74k forks source link

Implementation assembly load fails when ref assembly load has failed in a previous attempt #53268

Open Shyam-Gupta opened 3 years ago

Shyam-Gupta commented 3 years ago

Description

FileLoadException is thrown during assembly load when in a previous attempt assembly load for a reference assembly has failed with BadImageFormatException. Both implementation assembly and reference assemblies have same name.

Repro Steps: Consider following code snippet:


        const string userControlDll = "UserControl11.dll";

        var executingAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var refAssemblyPath = Path.Combine(executingAssemblyDirectory, "ref", userControlDll);

        Exception exception = null;

        Console.WriteLine("Loading ref assembly");

        try
        {
            AssemblyLoadContext.Default.LoadFromAssemblyPath(refAssemblyPath);
        }
        catch (BadImageFormatException ex)
        {
            // This is expected
            exception = ex;
        }

        Console.WriteLine("Loading implementation assembly");

        var implementationAssemblyPath = Path.Combine(executingAssemblyDirectory, userControlDll);

        try
        {
            AssemblyLoadContext.Default.LoadFromAssemblyPath(implementationAssemblyPath);
        }
        catch (Exception ex)
        {
            // Throws 'System.IO.FileLoadException' with error message "Assembly with same name is already loaded"
            // This seems unexpected
            exception = ex;
        }

Both the implementation assembly and reference assembly have same name. We first try to load reference assembly which fails with BadImageFormatException and is expected. However later when we try to load correct implementation assembly it fails with System.IO.FileLoadException message: "Assembly with same name is already loaded". Since the ref assembly load failed in previous attempt, it seems weird that implementation assembly load fails with this error.

Configuration

Scenario affected

This bug is causing issues with WinForms designer, which tries to load project output dlls to resolve custom types. In .NET 5.0 and .NET 6.0, project output folder contains ref binaries. When the designer's child process tries to load ref binaries, it fails with BadImageFormatException. It ignores those exceptions and then tries to load implementation assemblies which also fails.

ghost commented 3 years ago

Tagging subscribers to this area: @vitek-karas, @agocke, @coffeeflux, @vsadov See info in area-owners.md if you want to be subscribed.

Issue Details
### Description `FileLoadException` is thrown during assembly load when in a previous attempt assembly load for a reference assembly has failed with `BadImageFormatException`. Both implementation assembly and reference assemblies have same name. **Repro Steps:** Consider following code snippet: ```csharp const string userControlDll = "UserControl11.dll"; var executingAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var refAssemblyPath = Path.Combine(executingAssemblyDirectory, "ref", userControlDll); Exception exception = null; Console.WriteLine("Loading ref assembly"); try { AssemblyLoadContext.Default.LoadFromAssemblyPath(refAssemblyPath); } catch (BadImageFormatException ex) { exception = ex; } Console.WriteLine("Loading implementation assembly"); var implementationAssemblyPath = Path.Combine(executingAssemblyDirectory, userControlDll); try { AssemblyLoadContext.Default.LoadFromAssemblyPath(implementationAssemblyPath); } catch (Exception ex) { exception = ex; } ``` Both the implementation assembly and reference assembly have same name. We first try to load reference assembly which fails with `BadImageFormatException` and is expected. However later when we try to load correct implementation assembly it fails with `System.IO.FileLoadException` message: "Assembly with same name is already loaded". Since the ref assembly load failed in previous attempt, it seems weird that implementation assembly load fails with this error. ### Configuration * Which version of .NET is the code running on? .NET 5.0, .NET 6.0 ### Scenario affected This bug is causing issues with WinForms designer, which tries to load project output dlls to resolve custom types. In .NET 5.0 and .NET 6.0, project output folder contains ref binaries. When the designer's child process tries to load ref binaries, it fails with `BadImageFormatException`. It ignores those exceptions and then tries to load implementation assemblies which also fails.
Author: Shyam-Gupta
Assignees: -
Labels: `area-AssemblyLoader-coreclr`, `untriaged`
Milestone: -
Shyam-Gupta commented 3 years ago

FYI @merriemcgaw

agocke commented 3 years ago

@vitek-karas, could you look at this?

merriemcgaw commented 3 years ago

Chiming in here to say that this winds up blocking a key scenario in the WinForms designer, something our customers certainly expect to work. We would greatly appreciate the prioritization here 😄

agocke commented 3 years ago

Good to know, let's see if we can get to the bottom of this ASAP. Do you know if you'll need fixes for .NET 5 or earlier, or just .NET 6?

vitek-karas commented 3 years ago

I'm looking, but I doubt it's a recent bug.

That said - why do you need to load the ref assembly by the runtime? If it's only for "reflection-only-load" it would be much cleaner to use MetadataLoadContext which has no interactions with the runtime and will avoid the issue for sure.

Shyam-Gupta commented 3 years ago

The WinForms designer needs to load project output assembly to resolve custom user types which show up as controls in the Toolbox. To do this, the designer's child process (.NET Core process) loads all assemblies present in project output folder. In case there is an easy way to differentiate between ref assemblies and implementation assemblies then we can skip loading ref assemblies. Let us know.

agocke commented 3 years ago

I believe there's an attribute, System.Runtime.CompilerServices.ReferenceAssemblyAttribute, that should be on every reference assembly.

Shyam-Gupta commented 3 years ago

Is there any way to read assembly attributes without loading the assembly ? Else we will need to load each assembly into MetadataLoadContext first which will likely impact perf.

vitek-karas commented 3 years ago

MetadataLoadContext should be pretty fast – it’s lazy – meaning that it will only parse the bare headers and almost nothing else. You can go even lower level and use System.Reflection.Metadata and use the low level readers. It should be reasonably easy to read just assembly level attributes with that approach as well. @MichalStrehovsky for details on the low-level APIs if you're interested in that approach.

Note about MetadataLoadContext - it's purely managed code library, it has no interaction with loading in the runtime. So among other things, if you get rid of all managed references to the returned objects, the loaded stuff will simply disappear (via GC, just like any other managed objects).

vitek-karas commented 3 years ago

I checked what happens in the runtime in this case:

The failure happens basically during the "execution" phase - so it's similar to things like corrupted IL and so on. So the runtime treats the assembly as already loaded.

That said the runtime does basically the same thing you would do with MetadataLoadContext and it you would not be able to unload these, unlike the MetadataLoadContext where for assemblies which you don't want, they will simply go away.

agocke commented 3 years ago

Checking using System.Reflection.Metadata is pretty easy and should be cheap -- I wrote up a quick version here https://gist.github.com/agocke/f6a80c71cc62074379e2c9c4025505b7