Skip to content

Avoid reading and writing global state when loading native library #1563

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

Merged
merged 14 commits into from
Apr 17, 2018

Conversation

tmat
Copy link
Contributor

@tmat tmat commented Apr 12, 2018

Instead of using PATH environment variable to locate the native DLL, call LoadLibrary explicitly on the path the DLL should be loaded. DllImport will use the already loaded DLL and won't attempt to load it again.

The reason why it is not safe to change PATH environment variable include race-conditions and security concerns in plugin environments, such as msbuild.
For example, if two independently developed msbuild tasks that use different versions of the libgit2 library are loaded to the same msbuild process adds path to the native library to the PATH environment variable there is a chance that one of the writes doesn't make it causing one of the tasks to fail to find the library.

@tmat tmat force-pushed the NativeLoad branch 3 times, most recently from 00d51ab to 9398e8b Compare April 12, 2018 02:06
@@ -16,3 +17,5 @@
// The following GUID is for the ID of the typelib if this project is exposed to COM

[assembly: Guid("c6f71967-5be1-49f5-b48e-861bff498ea3")]

[assembly: InternalsVisibleTo("LibGit2Sharp.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010013172CAC3D61EF825164EF8443ED2F97316D0C2A4A65D3B2F6E5C9175C6C589D6A0EAE803E3E7FC0DA9E6672B1DE036CF74E1D33E21DD83E1145E3A454F92E52107495082DCCD1D9F521592F79F41DF26ED727059F8A4E5D3C23ECC525306831A15F1E56B693FDE112137E973B599A13209A5B63E05EE00886DE594E70A993B5")]
Copy link
Contributor

@AArnott AArnott Apr 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noooooo.........

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What's the problem with IVT to tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From another comment I left:

IVT allows unbounded and unchecked access to all internals which is decidedly against the pattern these tests use so far. There is great value (in my experience) in limiting tests to public API and having IVT will make that boundary much harder to preserve.

Value includes:

  • focusing on user-impacting behaviors rather than internal ones
  • no need to fix tests when you refactor code
  • justification to fix a failing test is straightforward since it generally ties directly to a user scenario instead of some arcane internal behavior that's hard to explain to your manager.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I instead add a new public API that returns the name of the native module, so that the tests can use it?

Copy link
Contributor Author

@tmat tmat Apr 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, the tests already link a couple of files from the product as a workaround for not being able to use those internal APIs. This workaround invalidates some of your points, although I agree it's more limited.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, the tests already link a couple of files from the product as a workaround for not being able to use those internal APIs.

That's unfortunate. Can you raise an issue pointing to those?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking a few source files with static types in both product and test assemblies isn't ideal, but in my experience it is far preferable to IVT because (as you point out) it severely limits the scope so we don't end up with half our tests written against internals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll try to make it work without IVT.

@@ -5,6 +5,8 @@
<OutputPath>$(MSBuildThisFileDirectory)bin\$(MSBuildProjectName)\$(Configuration)\</OutputPath>
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
<DefineConstants Condition=" '$(ExtraDefine)' != '' ">$(DefineConstants);$(ExtraDefine)</DefineConstants>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\libgit2sharp.snk</AssemblyOriginatorKeyFile>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears you moved this from the .csproj, which is a directory deeper. But you didn't modify the relative path, so I think this is pointing at a non-existent file now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you are right. Strange it's passing. Will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is actually correct. Because the path is relative to the project file. I'm gonna normalize it to a full path instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ya. Will you be using $(MSBuildThisFileDirectory) instead then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

var loadFromDirectory = args[1];
var expectedPath = Path.Combine(loadFromDirectory, (IntPtr.Size == 4) ? "x86" : "x64", moduleName + ".dll");

GlobalSettings.NativeLibraryPath = loadFromDirectory;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it required that the NetFx host app now help libgit2sharp find the right binary now? That's unfortunate. Is there no way to avoid it? Can't the library encode the logic for how to find the DLL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. it's not required. This is a test app that validates that setting GlobalSettings.NativeLibraryPath works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, at the very least, TestApp is not a very descriptive name. (What does it test? Is it actually running as part of our test gauntlet?)

Let's at least give it a better name. But having it actually run as part of the tests would be much more useful. If we can't do that, then I'm not sure we should be checking it in at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, sorry. You're consistently a step ahead of me. I do now see the test executing this.

Still, let's improve the name here a bit. NativeLoaderTest or something? If we add additional apps, it will be useful to disambiguate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename it to NativeLoaderTestApp. It is used by the new test I added. To test library loading we need to spawn a process, the test app is used for that.

</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\LibGit2Sharp\LibGit2Sharp.csproj" />
<ProjectReference Include="..\LibGit2Sharp.TestApp\LibGit2Sharp.TestApp.csproj" Condition="'$(TargetFramework)' == 'net461'" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you add ReferenceOutputAssembly=false metadata to this project reference so it doesn't proffer Program from the .exe into the test project?

Copy link
Contributor Author

@tmat tmat Apr 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the .exe to be referenced by the test project. I find the .exe by typeof(TestApp).Assembly.Location.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, it'd probably be better to not add that reference and just assume it get's copied next to the test assembly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a reference is good to have. If the test assembly requires the other assembly to be present at runtime, a project reference will ensure it gets built. Otherwise on a clean repo if I build the tests and run them, they'll fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant use ReferenceOutputAssembly=false

@@ -2,11 +2,11 @@

<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
<DefineConstants Condition=" '$(TargetFramework)' == 'net461' ">$(DefineConstants);DESKTOP</DefineConstants>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure no one is using the DESKTOP symbol?
Why remove it in favor of using NET461? Doing so means if later we target net462 that all the existing #if directives have to be changed when semantically they just need to know if they're targeting desktop.

Copy link
Contributor Author

@tmat tmat Apr 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure no one is using the DESKTOP symbol?

Yes. Of course I searched for it. It was used in one place.

Why remove it in favor of using NET461?

NET461 is a standard symbol. I can leave DESKTOP but imo it's better to use standard symbols rather then introducing new ones.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I respect the desire to use symbols that folks will recognize. I just do multi-targeting and retargeting so much and DESKTOP almost always covers my needs in a consistent, unchanging way, even when I have to target multiple desktop frameworks simultaneously.
I'm OK either way though.

@@ -19,8 +19,6 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\LibGit2Sharp\Core\Epoch.cs" Link="TestHelpers\Epoch.cs" />
<Compile Include="..\LibGit2Sharp\Core\Platform.cs" Link="TestHelpers\Platform.cs" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can these be removed? Is it because you added InternalsVisibleTo? That seems irrelevant to this change and IMO should be omitted.
FWIW, I strongly prefer linking in the source for a few static types over using InternalsVisibleTo. IVT allows unbounded and unchecked access to all internals which is decidedly against the pattern these tests use so far. There is great value (in my experience) in limiting tests to public API and having IVT will make that boundary much harder to preserve.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can these be removed? Is it because you added InternalsVisibleTo? That seems irrelevant to this change and IMO should be omitted.

Yes. It's not irrelevant since leaving these creates duplicate types and compiler errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's solving a problem your IVT change created. Can we revert the IVT change instead?

@@ -49,5 +52,35 @@ public void TryingToResetNativeLibraryPathAfterLoadedThrows()

Assert.Throws<LibGit2SharpException>(() => { GlobalSettings.NativeLibraryPath = "C:/Foo"; });
}

[ConditionalFact(typeof(NetFramework))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use xunit.skippablefact in this project. Can you use that instead of bringing in a new way to skip facts?

[SkippableFact]
public void LoadFromSpecificPath()
{
   Skip.IfNot(Platform.IsRunningOnNetFramework, ".NET Framework only test.");
   // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i didn't see that.

public void LoadFromSpecifiedPath()
[SkippableTheory]
[InlineData(new object[] { "x86" })]
[InlineData(new object[] { "x64" })]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think you can just say [InlineData("x86")] (and skip the array wrapping syntax).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Not sure why I thought it needs to be object.

@tmat
Copy link
Contributor Author

tmat commented Apr 13, 2018

@AArnott turns out that we'll need to explicitly load the native library on .NET Core as well. Currently .deps.json only works for the main app binary, so it won't be applied to msbuild task binaries :(.

@bording
Copy link
Member

bording commented Apr 13, 2018

@tmat That's one of the reasons why this approach makes much more sense IMHO: https://www.natemcmaster.com/blog/2017/11/11/build-tools-in-nuget/

Make it a console app that gets called at build time via targets instead.

@tmat
Copy link
Contributor Author

tmat commented Apr 13, 2018

@bording That has bad performance.

@tmat
Copy link
Contributor Author

tmat commented Apr 13, 2018

Unless it's a server app, but that's much more complicated than loading the native dependency. I'll continue working on this PR to make it work for Core CLR as well.

@bording
Copy link
Member

bording commented Apr 13, 2018

Unless it's a server app, but that's much more complicated than loading the native dependency. I'll continue working on this PR to make it work for Core CLR as well.

Given that we need to rely on the deps.json file to load the correct native library, which can vary by linux distro, how are you going to mimic that? You'd need knowledge of the RID to know which library to choose.

@tmat
Copy link
Contributor Author

tmat commented Apr 13, 2018

@bording I don't know yet. Will figure something out. Meanwhile, can we merge this PR? It's fixing it at least for .NET Framework.

@bording
Copy link
Member

bording commented Apr 13, 2018

@tmat I'll do a review this weekend, and I know @ethomson will likely want to take a look as well before it gets merged.

@tmat
Copy link
Contributor Author

tmat commented Apr 13, 2018

@bording Excellent. Thanks!

@AArnott
Copy link
Contributor

AArnott commented Apr 14, 2018

Ya, I have a few MSBuild tasks with dependencies (including Roslyn itself) that are very hard to manage, even without thinking about native libraries. I tried appdomains, load contexts, assembly resolvers, ... but getting the task to work on MSBuild Core and MSBuild (full) was virtually impossible. That's why CodeGeneration.Roslyn finally fell back to invoking a Core CLR console app that ships all its dependencies. But ya, perf is really bad (Roslyn JITing, rescanning all the assemblies repeatedly, etc.) compared to when it was working all in-proc with the MSBuild task. :(

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

@bording I think we can keep it simple for now: the RIDs libgit2sharp.nativebinaries uses are: linux-x64, osx, win7-x64 and win7-x86. These can be easily determined from info returned by RuntimeInformation API. More can be added as needed.

@AArnott
Copy link
Contributor

AArnott commented Apr 14, 2018

Consider voting up any of my MSBuild issues requesting better support for this, BTW.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

Future Core CLR runtimes will likely have better support for loading native libraries. Then we can remove the explicit handling.

@bording
Copy link
Member

bording commented Apr 14, 2018

@bording I think we can keep it simple for now: the RIDs libgit2sharp.nativebinaries uses are: linux-x64, osx, win7-x64 and win7-x86. These can be easily determined from info returned by RuntimeInformation API. More can be added as needed.

@tmat That's true of the current version of LibGit2Sharp.NativeBinaries, but I'm just waiting for the RID changes I got into corefx to show up in a 2.0.x release before I can finish up libgit2/libgit2sharp.nativebinaries#51.

The problem right now is that the single "linux" library in the NativeBinaries package only really works on Ubuntu. I want to update it to include support for every linux distro that .NET Core runs on, so that will be coming soon.

@bording
Copy link
Member

bording commented Apr 14, 2018

To provide a bit more info, at the very least, there's going to need to be a separate native binary for the CentOS family of distros since they use a completely different soname for their OpenSSL library.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

@bording Will it not be possible (hard) to detect the RID for the distros at runtime?

@bording
Copy link
Member

bording commented Apr 14, 2018

@tmat I've not spent time to determine how/if that is possible. Previously, the plan has always been to rely on .NET Core itself to figure that out vs. trying to re-implement that kind of logic directly. Aside from the MSBuild task scenario, I can't think of reason we'd want to.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

@bording MSBuild is the scenario we want it for :)

@bording
Copy link
Member

bording commented Apr 14, 2018

@bording MSBuild is the scenario we want it for :)

@tmat Yeah, I realize that. 😄

My point is, aside from MSBuild, is there a reason for LibGit2Sharp to need to have RID-aware, cross-platform, manual library loading for .NET Core? I can't think of one.

To me, that feels like instead of trying to add that sort of logic here, we should be looking at adding the missing features to MSBuild instead. That way everyone benefits from the effort.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

Yes, the missing features should be added to Core CLR and MSBuild. That's hopefully gonna happen in future, but for now libgit2sharp is unusable in msbuild tasks. I'd rather do something that's not overly complicated (and I believe this isn't) that makes it work while waiting for the real fix.

@AArnott
Copy link
Contributor

AArnott commented Apr 14, 2018

for now libgit2sharp is unusable in msbuild tasks

Why do you say that, @tmat? Nerdbank.GitVersioning contains an MSBuild task that invokes libgit2sharp right now and has for many months, and runs on full MSBuild as well as MSBuild Core on all OSs.

@bording
Copy link
Member

bording commented Apr 14, 2018

Yes, the missing features should be added to Core CLR and MSBuild. That's hopefully gonna happen in future, but for now libgit2sharp is unusable in msbuild tasks.

I would disagree that it's "unusable" in MSBuild tasks. People are already doing that right now. GitVerson and @AArnott's own Nerdbank.GitVersioning uses it in MSBuild tasks. There's even a GitVersion PR open that's got it working under .NET Core: GitTools/GitVersion#1269

I'd rather do something that's not overly complicated (and I believe this isn't) that makes it work while waiting for the real fix.

Sure, if you think that's possible without having to completely reimplement all of .NET Core's internal logic around RID parsing, etc. and still be able to handle the scenario of choosing the correct distro-specifc RID, then I'd like to see what that looks like.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

@bording Yeah, it "works" until you change the RIDs (or add more Linux flavors). See Nerdbank.GitVersioning:

https://github.com/AArnott/Nerdbank.GitVersioning/blob/master/src/NerdBank.GitVersioning/GitExtensions.cs#L357-L377

It'd be better if we had the RID logic in one place, where we know what RIDs do we need to support.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

It also needs to change the PATH environment variable, which is not a good thing to do in msbuild process.

@tmat
Copy link
Contributor Author

tmat commented Apr 14, 2018

Added commit that implements the loading for .NET Core. Tested on Windows (in msbuild task). Not yet on Linux/MacOS.

@tmat
Copy link
Contributor Author

tmat commented Apr 16, 2018

I believe I addressed all feedback now.

@ethomson
Copy link
Member

@AArnott, @bording do you have any final thoughts on this?

@bording
Copy link
Member

bording commented Apr 16, 2018

@ethomson I'd like to do another review pass to take a look at the new changes. Should be able to get to that later today.

@bording
Copy link
Member

bording commented Apr 17, 2018

@tmat Allowing the NativeLibraryPath to work with .NET Core but not have a default value seems like a good middle ground, though that does mean the "which library should I choose based on my RID?" problem is now something that has to be solved by the caller.

Also, regarding:

BTW, I noticed the unique id generator is generating a new guid every build. That's unfortuante since it makes the entire build non-deterministic. The guid seems to be only passed to the custom marshaller as a cookie, which doesn't use it for anything. So it seems unnecessary.

That does at least have a historical purpose. It's there to work around a CLR bug apparently: #438 (comment). If that caching bug has been fixed for all the frameworks that we run on now, then it seems like it could be removed.

@tmat
Copy link
Contributor Author

tmat commented Apr 17, 2018

though that does mean the "which library should I choose based on my RID?" problem is now something that has to be solved by the caller.

Yes, that's why I originally put it to this PR.

@tmat
Copy link
Contributor Author

tmat commented Apr 17, 2018

@bording Thanks for review!

@bording
Copy link
Member

bording commented Apr 17, 2018

Yes, that's why I originally put it to this PR.

Are you aware of any way to determine the RID at runtime other than having to construct it manually the same way the host does? If that was possible, then it would be more feasible to have a reliable default value for .NET Core.

@tmat
Copy link
Contributor Author

tmat commented Apr 17, 2018

That does at least have a historical purpose. It's there to work around a CLR bug apparently: #438 (comment). If that caching bug has been fixed for all the frameworks that we run on now, then it seems like it could be removed.

Could the MarshalCookie be the commit sha instead of a random guid?

@tmat
Copy link
Contributor Author

tmat commented Apr 17, 2018

@tmat
Copy link
Contributor Author

tmat commented Apr 17, 2018

https://github.com/dotnet/corefx/issues/16629

@bording
Copy link
Member

bording commented Apr 17, 2018

Could the MarshalCookie be the commit sha insted of a random guid?

It seems like that would work, at least for actually shipped assemblies. You could still run into a problem if you loaded two different versions of the assembly that you built locally, though.

However, what I'd really like to know is if it's even still a problem any more. It was mentioned in #430 (comment) that it was filed as a CLR bug. Do you by any chance have a way to find out if it ever got fixed?

@eerhardt
Copy link

I don't think there is public API for RID currently. There is this:
https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.DotNet.PlatformAbstractions/RuntimeEnvironment.cs

That is the public API currently. You can get it here: https://www.nuget.org/packages/Microsoft.DotNet.PlatformAbstractions/

@AArnott
Copy link
Contributor

AArnott commented Apr 17, 2018

Could the MarshalCookie be the commit sha insted of a random guid?

It seems like that would work, at least for actually shipped assemblies. You could still run into a problem if you loaded two different versions of the assembly that you built locally, though.

Even two versions built locally would have different commit IDs, except when you're building uncommitted changes multiple times, but how would that ever be a problem in practice?

Since you're using Nerdbank.GitVersioning, you should be able to get the commit ID from a build property, or a unique version from the ThisAssembly.AssemblyInformationalVersion at runtime.

@ethomson ethomson merged commit 8f6ad7c into libgit2:master Apr 17, 2018
@ethomson
Copy link
Member

Thanks again, everyone, this is now published at https://www.nuget.org/packages/LibGit2Sharp/0.26.0-preview-0017

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

Successfully merging this pull request may close these issues.

5 participants