Skip to content

AssemblyLoadContext: requiring full cooperation to stay "inside" is fundamentally flawed #45285

Closed as not planned

Description

Description

My overall scenario is that there is a native app, and I want to use .NET 5 C# to write a plugin for that app (see also: #1633). I have tried porting to .NET Core before, but each time I've run into significant problems. I just made another attempt, and I was excited to try to use the ability of AssemblyLoadContext to be unloaded, so that my plugin would have the same behavior as it already does in full .NET, where it can be unloaded (although I think it will require use of a shim DLL that does always stay loaded, because I can't specify an ALC to start in from the native side). But I ran into a significant problem even getting my code to work correctly, and I realize this will also completely preclude me from using collectible ALCs in the future as well.

I followed the hostfxr sample to write code to boot up the CLR and then use load_assembly_and_get_function_pointer_fn to get a pointer I can call to run my C# code. I learned that will load my code in an isolated context (“Calling this function will load the specified assembly in isolation”).

And what's wrong with that? Being in an IsolatedComponentLoadContext doesn't seem like such a bad thing, right?

But my code was malfunctioning, in a baffling way, until @jkotas helped me realize that my code was being loaded multiple times, in different ALCs. My plugin hosts the PowerShell runtime (7.1, built on .NET 5), and in PowerShell, when you load a [binary] module (Import-Module foo.dll), it just uses Assembly.LoadFrom("foo.dll") to load it... and Assembly.LoadFrom uses AssemblyLoadContext.Default to load into. So without trying, my code accidentally "escaped" the "isolated" ALC it had been originally loaded into (and hilarity ensued).

So: the current design of .NET/ALCs is that if you want a body of code to run in a separate, non-default ALC, then all that code must be purpose-written to stay in that ALC. Staying "inside" the "current" ALC is not something that happens by default; you have to do something special to not accidentally "escape" into the default ALC.

This is a deep, fundamental flaw.

Because as the amount of code that you want to run in a separate ALC grows, the chance of depending on some code that accidentally falls out of the ALC approaches 1. This is a serious crack in the foundation. As you put more code on top of it, the foundation will fail. Please change the design of loading/ALCs to make "staying in the current" ALC the default!

See also:

#41625: AssemblyLoadContext does not provide proper isolation of types
#598: Add support to specify the ALC in which to generate a dynamic assembly.

#13472: There's no way to call native library resolution on a specific AssemblyLoadContext (comment):

"However, isolation & dynamic loading is (almost) useless if it relies on libraries being written in a special way to make them "isolation-friendly" or "dynamic-load-friendly": Even if you write an assembly specifically to be isolated, chances are that you'll have at least one NuGet dependency that hasn't been written specially to be isolated."

#29842: WIP API Proposal - AssemblyBuilder.DefineDynamicAssembly for AssemblyLoadContext

#32851: Direct assemblies away from IsolatedComponentLoadContext towards a single AssemblyLoadContext (in which someone realized they had to do something special to get everything in the same ALC)

(and there are probably more, but I got tired of looking through all the Issues)

Regression?

Yes; I have code on full .NET Framework that uses AppDomains and nothing ever gets accidentally loaded in some other AppDomain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status

      No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions