dotnet / runtime

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

.NET 8 fails to load System.Security.Permissions when dynamically loading plugin that references Framework #104587

Open yaakov-h opened 1 month ago

yaakov-h commented 1 month ago

Description

I need to remotely administer an IIS server, and Microsoft.Web.Administration is a long-standing solution which manages this via remote COM APIs.

The latest versions of Microsoft.Web.Administration ships for .NET Standard, but these builds have the remote-administration code compiled out, and instead just throw a NullReferenceException from ServerManager.OpenRemote.

The older version 7.0 of Microsoft.Web.Administration only ships for .NET Framework 2.0, but it works on .NET 8 if we supply System.Security.Permissions as well via NuGet.

However, the .NET 8 runtime fails to load System.Security.Permissions when operating inside of another AssemblyLoadContext. It only seems to succeed if the entrypoint project is what takes on the package references.

When debugging this the runtime does not even ask for System.Security.Permissions.dll. I cannot find anything from a dotnet-trace to explain why it is failing, and procmon64 suggests that the .NET Runtime does not even look for the file on disk, it just aborts early with an exception.

Reproduction Steps

On a Windows machine with IIS and the .NET 8 SDK installed:

  1. Clone https://github.com/yaakov-h/dotnet-repro-remote-webadmin
  2. Create the folder 'bin'
  3. cd bin
  4. .\repro.ps1 from PowerShell as Administrator (elevated)

Expected behavior

The script displays success in all 5 scenarios:

Actual behavior

The first two scenarios fail, the final three pass:

```plain PS> .\repro.ps1 Building project... Determining projects to restore... Restored C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\PluginHost\PluginHost.csproj (in 70 ms). 2 of 3 projects are up-to-date for restore. PluginInterfaces -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\PluginInterfaces.dll RemoteWebAdministrationPlugin -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\RemoteWebAdministrationPlugin.dll PluginHost -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\PluginHost.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:00.84 Running with Assembly.Load... AppDomain.CurrentDomain.AssemblyResolve: Microsoft.Web.Administration, Version=7.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 AppDomain.CurrentDomain.AssemblyResolve: System.Security.Permissions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'System.Security.Permissions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified. File name: 'System.Security.Permissions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' at Microsoft.Web.Administration.Configuration.CheckPermissions(IAppHostElement section) at Microsoft.Web.Administration.Configuration.GetSectionInternal(ConfigurationSection section, String sectionPath, String locationPath) at Microsoft.Web.Administration.Configuration.GetSection(String sectionPath) at Microsoft.Web.Administration.ServerManager.get_SitesSection() at Microsoft.Web.Administration.ServerManager.get_Sites() at RemoteWebAdministrationPlugin.ListRemoteIisSitesPlugin.DoSomething(String[] args) in C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\RemoteWebAdministrationPlugin\ListRemoteIisSitesPlugin.cs:line 19 at Program.
$(String[] args) in C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\PluginHost\Program.cs:line 53 Running with AssemblyLoadContext... PluginLoadContext.Load: Loading RemoteWebAdministrationPlugin from C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\RemoteWebAdministrationPlugin.dll PluginLoadContext.Load: System.Runtime failed to resolve to path PluginLoadContext.Load: PluginInterfaces loading from default context. PluginLoadContext.Load: Loading Microsoft.Web.Administration from C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\Microsoft.Web.Administration.dll PluginLoadContext.Load: mscorlib failed to resolve to path PluginLoadContext.Load: System.Console failed to resolve to path PluginLoadContext.Load: System failed to resolve to path PluginLoadContext.LoadUnmanagedDll: ole32.dll failed to resolve to path PluginLoadContext.Load: System.Configuration failed to resolve to path Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'System.Security.Permissions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified. File name: 'System.Security.Permissions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' at Microsoft.Web.Administration.Configuration.CheckPermissions(IAppHostElement section) at Microsoft.Web.Administration.Configuration.GetSectionInternal(ConfigurationSection section, String sectionPath, String locationPath) at Microsoft.Web.Administration.Configuration.GetSection(String sectionPath) at Microsoft.Web.Administration.ServerManager.get_SitesSection() at Microsoft.Web.Administration.ServerManager.get_Sites() at RemoteWebAdministrationPlugin.ListRemoteIisSitesPlugin.DoSomething(String[] args) in C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\RemoteWebAdministrationPlugin\ListRemoteIisSitesPlugin.cs:line 19 at Program.
$(String[] args) in C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\PluginHost\Program.cs:line 53 Rebuilding project with ProjectReference... Determining projects to restore... Restored C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\PluginHost\PluginHost.csproj (in 201 ms). 2 of 3 projects are up-to-date for restore. PluginInterfaces -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\PluginInterfaces.dll RemoteWebAdministrationPlugin -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\RemoteWebAdministrationPlugin.dll PluginHost -> C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\PluginHost.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:00.97 Running with Assembly.Load... Server '5FKTZT3' has 5 sites hosted in IIS. Running with AssemblyLoadContext... PluginLoadContext.Load: Loading RemoteWebAdministrationPlugin from C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\RemoteWebAdministrationPlugin.dll PluginLoadContext.Load: System.Runtime failed to resolve to path PluginLoadContext.Load: PluginInterfaces loading from default context. PluginLoadContext.Load: Loading Microsoft.Web.Administration from C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\bin\plugins\Microsoft.Web.Administration.dll PluginLoadContext.Load: mscorlib failed to resolve to path PluginLoadContext.Load: System.Console failed to resolve to path PluginLoadContext.Load: System failed to resolve to path PluginLoadContext.LoadUnmanagedDll: ole32.dll failed to resolve to path PluginLoadContext.Load: System.Configuration failed to resolve to path Server '5FKTZT3' has 5 sites hosted in IIS. Running with direct Type.GetType... Server '5FKTZT3' has 5 sites hosted in IIS. ```

Regression?

This worked using Assembly.Load in .NET Framework. likely due to System.Security.Permissions being part of the BCL.

Known Workarounds

Adding a direct reference from the plugin host to the plugin project seems to work as a workaround, though I have absolutely no idea why. This is also not a viable permanent/long term workaround, more of a strange oddity discovered whilst trying to reproduce the issue.

Additionally, using AppDomain.CurrentDomain.AssemblyResolve hook as shown here appears to be a workaround, but this seems to break the purpose and spirit of using AssemblyLoadContext.

Configuration

dotnet --info:

``` .NET SDK: Version: 8.0.300 Commit: 326f6e68b2 Workload version: 8.0.300-manifests.9e3391ed MSBuild version: 17.10.4+10fbfbf2e Runtime Environment: OS Name: Windows OS Version: 10.0.22631 OS Platform: Windows RID: win-x64 Base Path: C:\Program Files\dotnet\sdk\8.0.300\ .NET workloads installed: [maui-windows] Installation Source: VS 17.10.34916.146 Manifest Version: 8.0.21/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maui\8.0.21\WorkloadManifest.json Install Type: Msi [ios] Installation Source: VS 17.10.34916.146 Manifest Version: 17.2.8053/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.ios\17.2.8053\WorkloadManifest.json Install Type: Msi [maccatalyst] Installation Source: VS 17.10.34916.146 Manifest Version: 17.2.8053/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maccatalyst\17.2.8053\WorkloadManifest.json Install Type: Msi [aspire] Installation Source: VS 17.10.34916.146 Manifest Version: 8.0.0/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.aspire\8.0.0\WorkloadManifest.json Install Type: Msi [android] Installation Source: VS 17.10.34916.146 Manifest Version: 34.0.95/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.android\34.0.95\WorkloadManifest.json Install Type: Msi Host: Version: 9.0.0-preview.5.24306.7 Architecture: x64 Commit: a5cc707d97 .NET SDKs installed: 6.0.321 [C:\Program Files\dotnet\sdk] 6.0.423 [C:\Program Files\dotnet\sdk] 7.0.120 [C:\Program Files\dotnet\sdk] 7.0.317 [C:\Program Files\dotnet\sdk] 7.0.410 [C:\Program Files\dotnet\sdk] 8.0.106 [C:\Program Files\dotnet\sdk] 8.0.300 [C:\Program Files\dotnet\sdk] 9.0.100-preview.5.24307.3 [C:\Program Files\dotnet\sdk] .NET runtimes installed: Microsoft.AspNetCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.30 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.19 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 9.0.0-preview.5.24306.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.30 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.19 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 9.0.0-preview.5.24306.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.30 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.19 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 9.0.0-preview.5.24306.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation] Environment variables: Not set global.json file: C:\git\GitHub\yaakov-h\dotnet-repro-remote-webadmin\global.json Learn more: https://aka.ms/dotnet/info Download .NET: https://aka.ms/dotnet/download ```

N.B. this also fails on machines w/o .NET 9 previews.

Other information

No response

huoyaoyuan commented 1 month ago

How about registering AssemblyLoadContext.Resolving event?

Sharing the file layout may help diagnosing the issue.

yaakov-h commented 1 month ago

@huoyaoyuan that doesn't seem to affect it, I've tried using Load(name) and Assembly.LoadFrom(..) in that event and neither seem to have any effect.

For the file layout, in this repro:

C:\GIT\GITHUB\YAAKOV-H\DOTNET-REPRO-REMOTE-WEBADMIN\BIN
│   PluginHost.deps.json
│   PluginHost.dll
│   PluginHost.exe
│   PluginHost.pdb
│   PluginHost.runtimeconfig.json
│   PluginInterfaces.deps.json
│   PluginInterfaces.dll
│   PluginInterfaces.pdb
│
└───plugins
    │   Microsoft.Web.Administration.dll
    │   PluginInterfaces.dll
    │   PluginInterfaces.pdb
    │   RemoteWebAdministrationPlugin.deps.json
    │   RemoteWebAdministrationPlugin.dll
    │   RemoteWebAdministrationPlugin.pdb
    │   RemoteWebAdministrationPlugin.runtimeconfig.json
    │   System.Security.Permissions.dll
    │   System.Windows.Extensions.dll
    │
    └───runtimes
        └───win
            └───lib
                └───net8.0
                        System.Windows.Extensions.dll

In the real project, the plugin dir is not a subdir of the host program.

huoyaoyuan commented 1 month ago

I've tried using Load(name) and Assembly.LoadFrom(..) in that event and neither seem to have any effect.

I believe AssemblyLoadContext.LoadFromAssemblyPath is the correct method - loading the assembly from given file, into the given ALC.

yaakov-h commented 1 month ago

Looking over the output, the Resolving event doesn't even get called in the first place.

image

yaakov-h commented 1 month ago

This also seems to affect loading System.ServiceProcess.ServiceController when touching the Application Pool APIs.

yaakov-h commented 1 month ago

I still don't understand why this is happening the way that it is, but as someone trying very explicitly to load a .NET Framework into .NET 8 this is the only workaround that seems to do the trick:

static MyPlugin()
{
    AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
    {
        if (e.Name.StartsWith("System.Security.Permissions,", StringComparison.Ordinal) || e.Name.StartsWith("System.ServiceProcess.ServiceController,", StringComparison.Ordinal))
        {
            var myPath = Path.GetDirectoryName(typeof(MyPlugin).Assembly.Location)!;
            var assemblyPath = Path.Combine(myPath, new AssemblyName(e.Name).Name + ".dll");
            return Assembly.LoadFrom(assemblyPath);
        }
        return null;
    };
}
agocke commented 4 weeks ago

Hmm, not aware of any known issue that prevents this.

@elinor-fung any ideas?