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

Assembly redirection equivalent on netcore not working. #3408

Closed
matthid opened this issue Aug 1, 2017 · 43 comments
Closed

Assembly redirection equivalent on netcore not working. #3408

matthid opened this issue Aug 1, 2017 · 43 comments

Comments

@matthid
Copy link
Contributor

matthid commented Aug 1, 2017

Initial Bugreport fsprojects/Paket#2576

It seems the compiler is not emitting the correct reference when given the same reference multiple times in different versions (could be a SDK bug)

Repro steps

  1. dotnet new console -lang f#

  2. edit fsproj and add

    <PackageReference Include="Suave">
      <Version>2.1.1</Version>
    </PackageReference>
    <PackageReference Include="Suave.OAuth">
      <Version>0.9.0-pre2</Version>
    </PackageReference>
  1. edit Program.fs and add
open System
open Suave

[<EntryPoint>]
let main argv =
    printfn "ClientId: %s" OAuth.EmptyConfig.client_id
    printfn "Hello World from F#!"
    startWebServer defaultConfig (Successful.OK("Hello World!"))
    0 // return an integer exit code
  1. dotnet restore && dotnet run

Expected behavior

Program executes

Actual behavior

Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'Suave, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null'. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
   at Program.main(String[] argv)

Known workarounds

Use C#, because:

  1. dotnet new console

  2. edit csproj and add

  <ItemGroup>
    <PackageReference Include="Suave">
      <Version>2.1.1</Version>
    </PackageReference>
    <PackageReference Include="Suave.OAuth">
      <Version>0.9.0-pre2</Version>
    </PackageReference>
  </ItemGroup>
  1. edit Program.cs and add
using System;
using Suave;

namespace suavefail
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("ClientId: {0}", OAuth.EmptyConfig.client_id);
            Console.WriteLine("Hello World!");
            Suave.Web.startWebServer(Suave.Web.defaultConfig, (Successful.OK("Hello World!")));
        }
    }
}
  1. dotnet restore && dotnet run works

Related information

If you look at the generated assemblies you can see that the C# version references:
// Suave, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null (dotpeek -> no specific version)
The F# version references:
// Suave, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, which is clearly wrong. I guess it was just the first (or last) in the command line to the F# compiler.

  • Operating System: Doesn't matter apparently
  • Tooling: dotnet sdk 1.0.4
  • Severity: Probably quite high as you might have this situation after updating packages.
  • EDIT: Severity is probably more to medium as the issue can be worked around by increasing the assembly-version via an updated suave package (see discussion), however it is left to analyze how this behaves on transitives only.

/cc @enricosada

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

Also, I should mention that I tried to find a workaround by using assembly redirects (which is how you would do it in the full framework). However in this particular case it seems to be impossible to workaround because "Suave" is not strongly named. So I have not found any workaround whatsoever.

@cartermp
Copy link
Contributor

cartermp commented Aug 1, 2017

I can repro on 2.0.0-preview3-006890 as well.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Suave">
        <Version>2.1.1</Version>
    </PackageReference>
    <PackageReference Include="Suave.OAuth">
        <Version>0.9.0-pre2</Version>
    </PackageReference>
  </ItemGroup>

</Project>

cc @KevinRansom @dsyme

@KevinRansom
Copy link
Member

@matthid I have taken a look at this, I need to talk to some folk first, but I think F# is doing the right thing, unless there is special handling for unsigned versioned assemblies.

The SDK tells both compilers reference these two dll's:

/reference:C:\Users\kevinr.nuget\packages\suave\2.1.1\lib\netstandard1.6\Suave.dll
/reference:C:\Users\kevinr.nuget\packages\suave.oauth\0.9.0-pre2\lib\netstandard1.6\Suave.OAuth.dll

The Suave .dll has a version 0.0.0.0 :
image

The Suave.oath references suave.dll version 2.0.0
image

Effectively the assembly load for Suave.dll v2.0.0.0 should fail.

However ... your repro demonstrates that C# compiles and runs, and F# compiles and doesn't run so we sort of need to get to the bottom of that.

Anyway long story short this will take more investigation, and is kind of neat and quirky.

Kevin

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

I already thought that would be the case. In that case some compiler changes are probably required.

From your investigation (can you post the full compiler command lines?) my assumption is that C# compiler notices the different versions and takes the one from command line, while F# takes the latest one, which is wrong in this case because 0.0.0.0 is actually bewer than 2.0.0.0.

It didn't even occur to me that 0.0.0.0 is the actual assembly version of the 2.1.1 assembly. Good catch.

So a workaround might be a newer suave release with a higher assembly version. If that is indeed the case the issue is not as urgent as I thought

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

Just one more note on this. There was a discussion on slack if this could be considered a runtime bug as well. And you can argue that the runtime should use the deps.json file for loading assemblies which contains the correct entries. So it might make sense to ask one of the runtime guys about this as well...

@KevinRansom
Copy link
Member

KevinRansom commented Aug 1, 2017

@matthid
Okay here is what is happening:

The F# compiler unifies all assembly references to the highest observed assembly version. When it writes the reference it writes this unified highest observed reference.

In the compiler types are not versioned, they match solely based on simple assembly names and type name.

So ... we make a reference by getting the info from all assemblies passed in on the command line. Furthermore, we amend this set of versions, by importing all of the types from the referenced assemblies, including transitive dependencies.

So from the command line we get 0,0,0,0 for the Suave reference, but from Suave.OAth we see 2.0.0.0.

So we up raise our reference to 2.0.0.0

What I think is wrong here is that we don't give a build error saying I can't find assembly Suave,2.0.0.0. Although TBH If we don't use any Suave types via OAuth then the 0.0.0.0 reference would probably be okay.

I'm honestly conflicted about this ... at least it fails early and during testing, In-fact, I wouldn't mind the compiler being more proactive, on the other hand erroring because of unused references might be too strict.

@KevinRansom
Copy link
Member

@matthid no it is not a runtime bug.

The coreclr will load an assembly of the version or higher than the reference. If the assembly discovered is lower than the reference then it fails.

So the coreclr is doing the right thing.

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

If I remember it correctly, the runtime behavior of CoreClr is that either the same, or a higher version can satisfy a reference.

That would mean that 2.0 can satisfy 0.0, but 0.0 can't satisfy 2.0. Following, the behavior of the runtime would be correct, if the assembly on disk really has version 0.0.

Furthermore, we amend this set of versions, by importing all of the types from the referenced assemblies, including transitive dependencies.

I assume that step is missing from the C# compiler

@KevinRansom
Copy link
Member

KevinRansom commented Aug 1, 2017

@0x53A
Your understanding of coreclr binding matches mine.

The C# unification approach is slightly different, they seem to prefer to use the reference from the /r: assemblies rather than the transitive references from importing those types. Effectively they won't see Suave, 2.0.0.0. Although there is a question about what happens if you use an API from OAth that has types from Suave(2.0.0.0) on it's public surface.

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

So this basically means you can never decrease an assemblyversion while increasing the nuget package version.

/cc @haf because this happened in suave

Let me think about this a bit and give another answer tomorrow. I'm also not sure I'm liking this and I'm a bit sceptical what this means for transitive deps in different versions.

Tbh my first instinct is that nuget version should be honored, this is completely unexpected...

@KevinRansom
Copy link
Member

@matthid What I think you mean to say is:

"I think the reference should use the assembly version that matches the assembly of the file passed on the command line" I.e "do what the developer said".

I think that is a fine position to take.

However, In general, I would say this is a badly specified project, You have a dependence on an assembly that requires 2.0.0.0 of the dll and you provide 0.0.0.0 of the dll. If the author of OAuth had intended 0.0.0.0 to be the version he should have specified it.

I honestly believe the error here is that the compiler doesn't issue an error rather than that the refs are wrong .... let's face it ... you didn't know the version number had gone away, that would normally indicate some sort of bug.

@KevinRansom
Copy link
Member

The net40 version of the suave.dll has a version number 2.1.1.0 So I think this should be regarded as a nuget package authoring error.

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

The problem is: This issue shows that we are not guaranteed to get the package manager resolution at runtime, which feels wrong.

From a compilers perspective I agree (as it doesn't see packages) however when looking at it from a higher level I'm really really skeptical. We really need to test how it behaves when we only talk about transitives, where we cannot just "prefer" one or another in the compiler - we need the help of the runtime here and need a way to tell it which one to take.

If we go down this road this "hard" requirement needs to be documented somewhere (ie. that a higher nuget package version may only contain a higher assembly-version dll). At least I never saw that requirement anywhere and sure would have expected that it would work. In fact when using full dotnet it probably would work - at least with assembly redirects and strongly named assemblies.

@KevinRansom
Copy link
Member

KevinRansom commented Aug 1, 2017

@matthid

I think :
"I think the reference should use the assembly version that matches the assembly of the file passed on the command line" I.e "do what the developer said".
is a very defensible position.

Here is the rationalization for unifying up, and it is a rationalization and therefore an opinion.
Well the developer is using various different versions of that assembly in this single compilation, since we don't work with multiple versions we need to pick one. By convention higher versions are compatible with earlier versions. So the developer will ship the highest available version of the dll and everything will unify well."

Here is the rationalization for unifying to the ref passed on the command line list of references, and it too is a rationalization and therefore also an opinion.
Well the developer is using various different versions of that assembly in this single compilation, since we don't work with multiple versions we need to pick one. By convention the developer knows what he is doing and when he specified the references, he clearly meant to use the acme.dll passed on the command line and that is the one he will ship.

Here is the rationalization for erroring if the compiler needs to write a reference not in the list of assemblies on the command line list of references, and it too is a rationalization and therefore also an opinion.
Well the developer is using various different versions of that assembly in this single compilation, since we don't work with multiple versions we need to pick one. By convention the developer knows what he is doing and when he specified the references, he clearly meant to use the higher version of the acme.dll and that is the one he will ship. However, since he didn't add the version we think the correctness is undefined and so we should error.

The problem with making a change to what we do know is it is a breaking change. There are some projects that work now, will not work in the future, and some that don't work ... such as your example will start working.

The above is why I am conflicted, I can see arguments for all three approaches. Regardless of that ... the example you provided is clearly a bug, and I am glad the F# highlighted the bug for you I think C# being a bit sloppy there lead you to miss a clear bug in the Suave package, and also why in my heart I prefer the 3rd option.

Kevin

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

So this basically means you can never decrease an assemblyversion while increasing the nuget package version.

probably, yes.

But I strongly assume that, with a slightly different setup, the C# compiler will have the same issues.

In your example, the app started up. But as soon as suave tried to use anything from suave.odata, it would also blow up because it would try to load 2.0, which is greater than the already loaded 0.0.

So in that regard, the behavior of fsc may be preferable, at least it fails earlier.


I agree with kevin that in my opinion the best resolution would be to error out if a transitive dependency has a higher version than an explicit dependency.

@KevinRansom
Copy link
Member

@0x53A I'm talking myself into adding an error when we detect this, so it will fail even earlier. Do you think that makes me evil?

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

if anything, it would make you a saint.

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

@0x53A I already added code from Suave.OData, what you probably mean is calling code interacting with the transitive Suave code (via Suave.OData), so that the runtime needs to load "Suave 2.0.0.0" in this case.

Would be interesting to check if this is indeed the case.

So this basically means you can never decrease an assemblyversion while increasing the nuget package version.

probably, yes.

What I'm saying is I never read or seen that anywhere, and I read a lot of NuGet docs because of Paket :/
I'm still arguing that this same scenario works in full dotnet, so why are we now saying it makes sense that it no longer works?

I'm still on a different position. Take a step back. If you look at this globally than we have a resolution by the package manager via nuget-package versions.
This very same solution needs to be taken at runtime. No matter the actual binaries contained in those packages. Do you agree on that?

If yes, we need to figure out which components need to do what to ensure that. Clearly some important information is lost in the NuGet -> SDK -> Executing pipeline...

Note that if you don't agree on this global statement than we have (IMHO) a really big problem and it doesn't really matter what we decide to do in the compiler for this particular case...

So, I'm suggesting one of the following:

  • Please point me to a place/docs which clearly states that I'm only allowed to increase the assembly-version.
  • We need to create that place (Then package managers can help verify this when creating the package)
  • We allow this and make it work (ie. run- AND compile-time use the correct assembly). Which currently is my preferred variant (And I even agree that this looks like a packaging error).

Sorry for the confusion. At least I no longer get a heart attack on this issue and clarified the severity a bit ;)

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

@matthid Yes, that is what I meant. The consoleapp is compiled againstsuave.dll v0, so that works ok. but suave.oauth.dll is compiled against suave.dll v2, so will fail (as soon as it actually tries to load it, which it probably didn't in your example)

I did just check it.

ConsoleApp references lib1 and lib2.
Lib2 references Lib1 v2.

ConsoleApp references Lib1 v1.

The app starts, but as soon as Lib2 tries to call into lib1, it fails at runtime.

Here you can see that v1 is loaded into the process already, but lib2 needs v2:
image

consoleapp can call into both lib1 and lib2 successfully, but as soon as lib2 calls into lib1, it will fail.


I'm still arguing that this same scenario works in full dotnet

fullfx works quite different. If you don't have a SN, then the version doesn't matter at all. If you do have a SN, you need an exact match (or binding redirect).

corefx, the version always matters, but a higher can satisfy a lower.

This fixes a lot of issues, i like the new behavior.

@KevinRansom
Copy link
Member

Does it work on the desktop? I think it may do but only because the desktop loader is sloppier with versions for weakly named assemblies, although I don't have a lot of recent experience with weak named assembly binding.

For sure the desktop will complain that the manifest doesn't match if the assembly being loaded doesn't match the reference for strongly signed assemblies. It is true that binding redirects, Machine Policy and GAC policy can impact the reference.

The CORECLR doesn't have policy knobs such as binding redirects or machine policy or GAC policy. Instead it has a binding logic that unifies references up at runtime. It would seem that they follow that policy also for weak named assemblies. Which does seem like it may be a detectable change on the CORECLR binding policy.

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

Does it work on the desktop? I think it may do but only because the desktop loader is sloppier with versions for weakly named assemblies, although I don't have a lot of recent experience with weak named assembly binding.

afaik, it would probably work, afaik without sn the version is ignored and only the assembly name is relevant.


It would seem that they follow that policy also for weak named assemblies. Which does seem like it may be a detectable change on the CORECLR binding policy.

If this is a breaking change of the core-RUNTIME, then I would prefer FSC to notify me in an error condition. Otherwise I may get a nasty surprise at runtime

@KevinRansom
Copy link
Member

@0x53A --- I think you are right.

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

@matthid: this will fail, because protectedPart calls into Suave.dll

image

using System;
using Suave;

namespace suavefail
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("ClientId: {0}", OAuth.EmptyConfig.client_id);
            Console.WriteLine("Hello World!");
            OAuth.protectedPart(null, null);
            Suave.Web.startWebServer(Suave.Web.defaultConfig, (Successful.OK("Hello World!")));
        }
    }
}

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

@0x53A I'm not really convinced by this, there is no reason to not use the resolution from the package manager but instead try to find different versions (and fail at it).

However, this might be a fight which need to be fought at another place. So a change which prevents a runtime error and fails at compile time might be the right choice.

BUT, we need to be careful. I'm pretty sure you can hook into the runtime resolver and make this work (by adding a line or two at the start of "Main"). So we might break people which already have build their own solutions (Maybe even FAKE).

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

There is no package manager, there is just a flat list of dll files.

And yes, this would need to be fought at coreclr itself.


What I would like is a comeback of assembly redirects in some form. The default behavior of unifying up is great, and a vast improvement over fullfx, but a way to override this would sometimes be handy.

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

There is no package manager, there is just a flat list of dll files.

Yes there is ;). In my repro steps NuGet is resolving a perfectly valid set of nuget packages when you type dotnet restore... I expect that the same set of packages is used at runtime and - if required - other requests are forwarded.

@matthid
Copy link
Contributor Author

matthid commented Aug 1, 2017

I expect that the same set of packages is used at runtime and - if required - other requests are forwarded.

And no. I don't really care how it gets there ;). If we embed the result in the references metadata in the assembly or a binding redirects file or some similar solution doesn't really matter to me.

@0x53A
Copy link
Contributor

0x53A commented Aug 1, 2017

Ultimately, I would judge this a package authoring error. Versions go up, not down.

@KevinRansom
Copy link
Member

@matthid

The compiler did use exactly the packages and dlls specified in the project. However, those packages produced a non-working program.

On coreclr the loader has a different versioning heuristic. It is symmetrical between signed/unsigned and it says version go up and are expected to be compatible.

An error is probably the much preferred solution here, for the project you supplied, until it is fixed.

@rmunn
Copy link

rmunn commented Aug 2, 2017

I've reported this upstream as SuaveIO/suave#635. But I think this discussion over the right interplay between compiler, package manager, and runtime has value in its own right, so I'd be in favor of keeping this issue open until the discussion has reached a conclusion.

@matthid
Copy link
Contributor Author

matthid commented Aug 2, 2017

I hope we get more relevant information regarding the runtime internals via https://github.com/dotnet/coreclr/issues/13168. Apparently, assembly redirects should have worked - at least according to gitter.

So if binding redirects should work I'm not so sure about the compiler error anymore. Also you can easily make it work:

    class Program
    {
        static void Main(string[] args)
        {
            AssemblyLoadContext.Default.Resolving += Default_Resolving;
            OldMain();
        }

        private static void OldMain()
        {
            Console.WriteLine("ClientId: {0}", OAuth.EmptyConfig.client_id);
            Console.WriteLine("Hello World!");
            OAuth.protectedPart(null, null);
            Suave.Web.startWebServer(Suave.Web.defaultConfig, (Successful.OK("Hello World!")));
        }

        private static System.Reflection.Assembly Default_Resolving(AssemblyLoadContext arg1, System.Reflection.AssemblyName arg2)
        {
            if (arg2.Name == "Suave")
            {
                return typeof(Suave.Web).GetTypeInfo().Assembly;
            }
            return null;
        }
    }

I'm too lazy to implement the F# solution (because there is no good IDE), but I'm pretty sure it works in a similar way. You have to be a bit more careful, because the compiler itself is resolving the WRONG assembly, but it shouldn't matter (you just cannot use typeof like above).

Basically this is "binding-redirects" done brute-force programmatically. Note that you need to introduce the additional method because of the way how assembly loading works.

So should we forbid something which can be made work?

@KevinRansom
Copy link
Member

@matthid what makes you think that coreclr has binding redirects?

To the best of my understanding it doesn't ... the coreclr binding policy is this:
load an assembly with a matching simple name.
References to higher versions successfully bind.
References to lower versions fail.

Yes you can assembly handle resolution in application code. however, that is a super advanced solution and not one the compiler would get involved in.

@matthid
Copy link
Contributor Author

matthid commented Aug 2, 2017

@matthid what makes you think that coreclr has binding redirects?

I edited the issue already, apparently it was a mistake from gitter. Documentation around it is a bit sparse and I'd like to get pointed to docs ;)

Yes you can assembly handle resolution in application code. however, that is a super advanced solution and not one the compiler would get involved in.

My argument is that with a compiler error we'd make that completely impossible.

To be completely honest with you, currently I don't see any solution which makes me completely happy. I have wasted enough time in my life with assembly binding errors but still every solution has potential downsides. I guess we kind of have arranged ourself with binding redirects (in Paket world) to force the runtime to use our resolution, so I guess we are a bit unhappy that there is no replacement.

Maybe we just need to arrange ourself with this highest version philosophy. I don't know jet.

@KevinRansom
Copy link
Member

@matthid this higher philosophy is not new ... Silverlight has always had this binding policy,
msbuild and nuget always resolve upwards and auto generate binding redirects to the highest noticed version number. It's just that binding redirects allow a developer to force the bind to an earlier version on the desktop. That is gone ...

@cartermp
Copy link
Contributor

Closing as by design

@forki
Copy link
Contributor

forki commented Feb 14, 2018

Is there a bug that tracks that assembly redirects are needed on core?

@cartermp
Copy link
Contributor

@forki In which context?

@forki
Copy link
Contributor

forki commented Feb 14, 2018

Ok that question implies you don't think it's true ;-)
All I can say is that azure functions is currently a nightmare to develop because we can't control versions of the runtime. It does not mean that redirects are the solution - but they were the best thing we had. Now we have nothing.

@matthid
Copy link
Contributor Author

matthid commented Feb 14, 2018

@forki This all sounds like we have to hack our own thing again :)

@forki
Copy link
Contributor

forki commented Feb 14, 2018 via email

@cartermp
Copy link
Contributor

I think the root of the issue that you're having is that the (preview) Functions v2 runtime simply isn't ready for prime-time yet.

At any rate, unless @KevinRansom and @dsyme change their minds I don't see a reason to re-open this as it is explicitly marked as by design.

@forki
Copy link
Contributor

forki commented Feb 14, 2018 via email

@matthid
Copy link
Contributor Author

matthid commented Jun 10, 2019

For my future me. The above C# workaround in F# (as this happened again, see fsharp/fsharp-compiler-docs#906):

    System.Runtime.Loader.AssemblyLoadContext.Default.add_Resolving(Func<_,_,_> (fun arg1 arg2 -> 
      if arg2.Name = "FSharp.Compiler.Service"
      then typeof<FSharp.Compiler.Ast.Ident>.Assembly
      else null))

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

No branches or pull requests

7 participants