Runtime: PlatformNotSupportedException while using ProtectedData.Protect from a library loaded through reflection, on Windows

Created on 8 Jun 2020  路  13Comments  路  Source: dotnet/runtime

Description

We have a library that uses ProtectedData.Protect. If we load this library through reflection, we get PlatformNotSupportedException, on Windows. If we add the reference of ProtectedData directly in the app, that loads the library, things work fine.

Configuration

  • Using it in net core 3.1
  • Tried in Windows 10, x64

Regression?

  • Here is the sample application to reproduce the issue-
    SampleApp.zip

Let me know if you need any other information.

System.Security.Cryptography.ProtectedData

area-AssemblyLoader-coreclr untriaged

Most helpful comment

There are several things at play here:

No default probing in .NET Core

.NET Core by default doesn't probe on disk for assemblies. It follows the .deps.json which is produced by SDK. In this case, the consoleapp.deps.json doesn't mention S.S.C.ProtectedData, so the runtime will not know anything about that assembly when it starts.

Assembly.LoadFrom

Assembly.LoadFrom is an API which was intentionally kept to work similarly to .NET Framework. One of its features is that it adds default probing behavior. So calling Assembly.LoadFrom("myasm.dll") will load that file, but also if myasm.dll has additional dependencies those will be looked for next to myasm.dll. This is what happened in your case.

Reliance on .deps.json for RID specific assets

Unlike .NET Framework which didn't have any notion of RID specific assets, .NET Core adds this capability. It means that "portable apps" can carry multiple copies of an asset (assembly, or native library typically) which are specific to certain RIDs (target OS, version, architecture). There's no logic in the runtime itself to search for the right RID specific asset. Instead this information is stored in the .deps.json file. In this case the consoleapp.deps.json doesn't have anything on S.S.C.ProctedData since it's not a direct static dependency of the app. The right information needed is in sampleservice.deps.json. Assembly.LoadFrom doesn't use .deps.json in any way (it's just how the API works right now), so there was no way for the runtime to know about the RID specific assets.

AssemblyDependencyResolver

In .NET Core 3.0 we've added AssemblyDependencyResolver which is the component to use for dynamically loading additional code which can handle all of the intricacies of .deps.json. You give it the path to the assembly to load, it will look for .deps.json next to it and if it's there it will parse it and use it. Then you can call AssemblyDependencyResolver.ResolveAssemblyToPath to perform the assembly resolution based on the .deps.json. It's typically used to load "plugins" into isolated load contexts and we have an example of that here: https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin.

Combining this in your sample app, we just have to use AssemblyDependencyResolver and hook it up to the assembly resolution pipeline on the default load context. So I took your sample app and modified the Program.cs like this:
``` C#
Console.WriteLine("Calling Service DLL by reflection............");
// Console.WriteLine(GetProtectedData() + "\n");
Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(@"........serviceApp\bin\Debug\netcoreapp3.1\publishServiceApp.dll"));

        var resolver = new AssemblyDependencyResolver(asm.Location);
        AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext ctx, AssemblyName asmName) =>
        {
            string asmPath = resolver.ResolveAssemblyToPath(asmName);
            if (asmPath != null)
            {
                return ctx.LoadFromAssemblyPath(asmPath);
            }

            return null;
        };

        Type type = asm.GetType("ServiceApp.SampleService");
        if (type != null)
        {

```

Things to note:

  • Not using Assembly.LoadFrom as I don't want the "helpful" automatic probing for dependencies (which is wrong in this case)
  • Explicitly loading into Default load context (this is more of a future looking thing, LoadFrom always loads into Default load context as well, but if you plan on isolating the services, this should load into a secondary ALC per the sample mentioned above).
  • Hooking ALC.Default.Resolving event, which gives me a chance to resolve the otherwise unresolved dependency to S.S.C.ProtectedData.
  • Using the AssemblyDependencyResolver to perform the actual resolution - I don't need to know anything about the actual paths, RIDs and so on.

With the above changes I can just run the app and it works.

RID specific build

As mentioned above, if you build the app for a specific RID - typically dotnet publish -r win-x64 for example, this changes things. The app and the sample service component are no longer "portable" and so the SDK knows to only include assets for that specific RID. So the windows specific S.S.C.ProtectedData gets copied to the output, and only that one. There are no subfolders and so on. The .deps.json files are still there and will work, but they basically just all point to "root directory". That's why publishing to specific RID fixes the issues as well, since the "look next to it" probing of LoadFrom will suddenly start to work.

If you're planning to only run on Windows, then publishing to windows will fix the problem. That said I would still suggest for you to not use Assembly.LoadFrom as it's "magic" which might not always work for you. Using the AssemblyDependnecyResolver is obviously more code, but if you might ever move to isolated load contexts, it's effectively required then.

All 13 comments

I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label.

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq
Notify danmosemsft if you want to be subscribed.

I can reproduce it but I'm not sure what the right solution is. Gist of what I see:

  1. ServiceApp has a dependency on S.S.C.ProtectedData via PackageReference.
  2. Do a dotnet publish on serviceApp. This publishes the library, and among other things, puts a copy of S.S.C.ProtectedData.dll in the publish directory. This assembly is the lib copy of ProtectedData. This is the one that throws PNSE. The "real" windows one is copied in runtimes\win\lib
  3. ConsoleApp loads ServiceApp via reflection.
  4. ConsoleApp invokes a method in ServiceApp, that method uses S.S.C.ProtectedData.
  5. This causes the non-runtime-specific ProtectedData assembly to get loaded from the publish directory, not the one from runtimes\win.

It seems there are two fixes

  1. Add an explicit dependency to System.Security.Cryptography.ProtectedData so the right dependency is loaded.
  2. When using dotnet publish on ServiceApp, specify the runtime using dotnet publish --runtime win and adjust the reflection loaded path from serviceApp\bin\Debug\netcoreapp3.1\publish\ServiceApp.dll to serviceApp\bin\Debug\netcoreapp3.1\win\publish\ServiceApp.dll. Note the win in the path. Specifying the RID will cause the published app to include the specific runtime assets for that platform.

It worked, thanks!
Just curious, why couldn't it refer to the correct dll as per runtime in reflection while it could without reflection?

Someone more familiar with how reflection works could probably say, but it's likely because reflection hasn't been taught about runtime paths when probing for assemblies to load. It just finds the first assembly that matches, and in this case it picks the wrong one, the one that is not runtime-specific.

Oh I get that. Do you think this scenario can be a bug on reflection?

Do you think this scenario can be a bug on reflection?

I don't think it is a bug, but perhaps a limitation of how assembly probing works. If you think this behavior could be improved, perhaps a new feature request issue would be useful. I think this behavior can occur with just about any package that has RID-specific assets, not just ProtectedData.

I get that, it doesn't seem like an issue with ProtectedData. Can you tag someone from the reflection group to have a look into this?

/cc @GrabYourPitchforks

I repathed the area to the VM instead of _System.Security_ because I'm not quite sure where else to bucket this.

Tagging subscribers to this area: @vitek-karas, @agocke
Notify danmosemsft if you want to be subscribed.

There are several things at play here:

No default probing in .NET Core

.NET Core by default doesn't probe on disk for assemblies. It follows the .deps.json which is produced by SDK. In this case, the consoleapp.deps.json doesn't mention S.S.C.ProtectedData, so the runtime will not know anything about that assembly when it starts.

Assembly.LoadFrom

Assembly.LoadFrom is an API which was intentionally kept to work similarly to .NET Framework. One of its features is that it adds default probing behavior. So calling Assembly.LoadFrom("myasm.dll") will load that file, but also if myasm.dll has additional dependencies those will be looked for next to myasm.dll. This is what happened in your case.

Reliance on .deps.json for RID specific assets

Unlike .NET Framework which didn't have any notion of RID specific assets, .NET Core adds this capability. It means that "portable apps" can carry multiple copies of an asset (assembly, or native library typically) which are specific to certain RIDs (target OS, version, architecture). There's no logic in the runtime itself to search for the right RID specific asset. Instead this information is stored in the .deps.json file. In this case the consoleapp.deps.json doesn't have anything on S.S.C.ProctedData since it's not a direct static dependency of the app. The right information needed is in sampleservice.deps.json. Assembly.LoadFrom doesn't use .deps.json in any way (it's just how the API works right now), so there was no way for the runtime to know about the RID specific assets.

AssemblyDependencyResolver

In .NET Core 3.0 we've added AssemblyDependencyResolver which is the component to use for dynamically loading additional code which can handle all of the intricacies of .deps.json. You give it the path to the assembly to load, it will look for .deps.json next to it and if it's there it will parse it and use it. Then you can call AssemblyDependencyResolver.ResolveAssemblyToPath to perform the assembly resolution based on the .deps.json. It's typically used to load "plugins" into isolated load contexts and we have an example of that here: https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin.

Combining this in your sample app, we just have to use AssemblyDependencyResolver and hook it up to the assembly resolution pipeline on the default load context. So I took your sample app and modified the Program.cs like this:
``` C#
Console.WriteLine("Calling Service DLL by reflection............");
// Console.WriteLine(GetProtectedData() + "\n");
Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(@"........serviceApp\bin\Debug\netcoreapp3.1\publishServiceApp.dll"));

        var resolver = new AssemblyDependencyResolver(asm.Location);
        AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext ctx, AssemblyName asmName) =>
        {
            string asmPath = resolver.ResolveAssemblyToPath(asmName);
            if (asmPath != null)
            {
                return ctx.LoadFromAssemblyPath(asmPath);
            }

            return null;
        };

        Type type = asm.GetType("ServiceApp.SampleService");
        if (type != null)
        {

```

Things to note:

  • Not using Assembly.LoadFrom as I don't want the "helpful" automatic probing for dependencies (which is wrong in this case)
  • Explicitly loading into Default load context (this is more of a future looking thing, LoadFrom always loads into Default load context as well, but if you plan on isolating the services, this should load into a secondary ALC per the sample mentioned above).
  • Hooking ALC.Default.Resolving event, which gives me a chance to resolve the otherwise unresolved dependency to S.S.C.ProtectedData.
  • Using the AssemblyDependencyResolver to perform the actual resolution - I don't need to know anything about the actual paths, RIDs and so on.

With the above changes I can just run the app and it works.

RID specific build

As mentioned above, if you build the app for a specific RID - typically dotnet publish -r win-x64 for example, this changes things. The app and the sample service component are no longer "portable" and so the SDK knows to only include assets for that specific RID. So the windows specific S.S.C.ProtectedData gets copied to the output, and only that one. There are no subfolders and so on. The .deps.json files are still there and will work, but they basically just all point to "root directory". That's why publishing to specific RID fixes the issues as well, since the "look next to it" probing of LoadFrom will suddenly start to work.

If you're planning to only run on Windows, then publishing to windows will fix the problem. That said I would still suggest for you to not use Assembly.LoadFrom as it's "magic" which might not always work for you. Using the AssemblyDependnecyResolver is obviously more code, but if you might ever move to isolated load contexts, it's effectively required then.

@vitek-karas thanks a lot for all this explanations. It cleared a lot of doubts/confusions. I will try to follow the leads.

Was this page helpful?
0 / 5 - 0 ratings