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

Use high-level library load API #217

Open
wants to merge 21 commits into
base: main
Choose a base branch
from

Conversation

samardzicneo
Copy link

.NET provides multiple APIs for loading libraries. One of them is NativeLibrary.Load(string), which imports the library directly from the specified path. However, this requires the caller to search for the path and specify the proper extension for the OS. NativeLibrary.Load(string,Assembly,DllImportSearchPath) searches for the library in the usual library paths, like /lib, and adds extensions automatically.

@samardzicneo samardzicneo marked this pull request as draft March 7, 2024 18:39
@samardzicneo
Copy link
Author

Interesting to see that the CI fails, will mark as draft and look into it, @jasongin thanks for the pipeline run.

worked on my machine

@jasongin
Copy link
Member

jasongin commented Mar 7, 2024

Thanks for the PR, but I have a couple concerns:

  1. For now, we need the ability to explicitly specify a path to libnode. The plan is eventually to consume it via a nuget package (Build libnode and publish nuget packages #107) which will take care of placing the libnode binary in the same directory as the assembly. But until then, test code loads it directly from a fixed path in the repo.
  2. This code change doesn't build on all supported target frameworks. See the CI build errors. It is because there is a NativeLibrary.cs in the repo that implements the missing APIs for older versions of .NET, including .NET Framework 4.x.

@samardzicneo
Copy link
Author

@jasongin As for your first concern, this is still possible when using the "better" Load API. If you give it a path, it'll still load it like it did before. For your second concern, I've just pushed a commit which resolves the failing pipeline, and sorry for not running it in my forked repo before PR'ing.

@samardzicneo samardzicneo marked this pull request as ready for review March 7, 2024 20:24
if (Current != null)
{
throw new InvalidOperationException(
"Only one Node.js platform instance per process is allowed.");
}

nint libnodeHandle = NativeLibrary.Load(libnodePath);
#if NET7_0_OR_GREATER
Copy link
Member

Choose a reason for hiding this comment

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

Can this be !NETFRAMEWORK? The 3-parameter overload of NativeLibrary.Load() is present on .NET 6.

Copy link
Author

Choose a reason for hiding this comment

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

This code was removed in the newest revision.


nint libnodeHandle =
entryAssembly != null
? NativeLibrary.Load(libnode, entryAssembly, null)
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little concerned about the lack of functional parity on .NET Framework 4.x. Would it be feasible to add code in this repo's NativeLibrary.cs to at least replicate the logic for loading from the same directory as the assembly?

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented a partially-featured replica of the original method, but I can't test it because I don't have access to a Windows machine.

if (Current != null)
{
throw new InvalidOperationException(
"Only one Node.js platform instance per process is allowed.");
}

nint libnodeHandle = NativeLibrary.Load(libnodePath);
#if NET7_0_OR_GREATER
var entryAssembly = System.Reflection.Assembly.GetEntryAssembly();
Copy link
Member

Choose a reason for hiding this comment

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

Did you consider using DllImportSearchPath.AssemblyDirectory instead? I think technically that would use the current assembly rather than the entry assembly, but that might actually be better.

Copy link
Author

Choose a reason for hiding this comment

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

Even using AssemblyDirectory, you still need to pass an assembly. I'd rather use the system default but I can change the assembly to get the current assembly instead of the entry.

@jasongin
Copy link
Member

jasongin commented Mar 7, 2024

I see the build issues are fixed, thanks.

@samardzicneo, I'm curious what is your motivation for submitting this change? I see you're aware in #218 of the current unfinished state of libnode support.

@samardzicneo
Copy link
Author

samardzicneo commented Mar 7, 2024

@jasongin Thanks for the review, I'll have to look at your comments tomorrow.

Copy link
Member

@jasongin jasongin left a comment

Choose a reason for hiding this comment

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

Sorry this got more complicated than you probably anticipated, but we have to consider security.


nint libnodeHandle =
entryAssembly != null
? NativeLibrary.Load(libnode, entryAssembly!, null)
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to specify DllImportSearchPath.SafeDirectories here, because the default search behavior is considered unsafe.

If your goal was to load libnode from the same directory as the application, I think that will work, though the documentation for SafeDirectories is not entirely clear.

In that case the back-compat code in NativeLibrary should search the application directory, which is not necessarily the same as the assembly directory.

Copy link
Author

Choose a reason for hiding this comment

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

SafeDirectories won't use the assembly directory, so I'm ORing SafeDirectories and AssemblyDirectory now.

string?[] tryDirectories =
[
Path.GetDirectoryName(assembly.Location),
Environment.CurrentDirectory,
Copy link
Member

Choose a reason for hiding this comment

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

Do not search the current directory; that is very unsafe.

Copy link
Author

Choose a reason for hiding this comment

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

Removed.

/// <returns>Library path if found, otherwise false.</returns>
private static string? FindLibrary(string libraryName, Assembly assembly)
{
if (File.Exists(libraryName))
Copy link
Member

Choose a reason for hiding this comment

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

If libraryName is not an absolute path this searches the current directory (or a relative path from it), which is unsafe.

Copy link
Author

Choose a reason for hiding this comment

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

Added a check to ensure it's an absolute path.

@samardzicneo
Copy link
Author

@jasongin Thanks for the review. I wasn't aware that loading from cwd is unsafe.

Side note, this is my first time contributing to open source so I came into it fully expecting a load of comments. Especially if it concerns security, I'd rather have more than less comments.

? NativeLibrary.Load(
libnode,
entryAssembly!,
DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories
Copy link
Member

Choose a reason for hiding this comment

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

AssemblyDirectory is listed as one of the unsafe options, so this may get flagged by a security scan. (I admit I do not understand why that location should be considered unsafe.)

I think it would be better to allow the application to optionally specify the search path:

public NodejsPlatform(string libnode, string[]? args = null, DllImportSearchPath? searchPath = null)

Then the application developer can decide whether some search paths are unsafe or not, depending on the application threat model.

Copy link
Author

Choose a reason for hiding this comment

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

I can do that, but after some research it looks like CA5393 might have some problems in it: It looks like "SafeDirectories" includes the application directory, but then "ApplicationDirectory" is considered unsafe. Maybe this rule hasn't been updated in a while, or maybe it was made with only Windows in mind? On UNIX it's quite common to load libraries from the assembly directory and if it really was insecure, wouldn't that mean that .NETs own approach of putting managed dependency DLLs next to the assembly is also insecure?

If you want I can still change this but to me, the rule looks a little weird.

Copy link
Member

Choose a reason for hiding this comment

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

I learned today that the DllImportSearchPath is entirely ignored on non-Windows platforms, and that is not documented: dotnet/dotnet-api-docs#3205

In that case should we use your FindLibrary() code on those platforms? Or if not, will it even search in the right place(s)? It probably requires testing.

Also I found that CA5393 is now disabled by default since it doesn't apply cross-platform, so maybe we don't need to worry about that. (I recalled dealing with this warning in other projects several years ago.)

Copy link
Member

@jasongin jasongin Apr 6, 2024

Choose a reason for hiding this comment

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

At least for the near future, loading libnode from a system directory is rarely going to be the right choice. The Node.js project does not publish official libnode shared-library binaries, and I believe most applications that do use it now are static-linked. Also, applications are likely to be sensitive to the major version of node. So any apps that do use a libnode shared-library will distribute it with the application.

For this project, our plan is to publish libnode binaries via NuGet packages. Then when a .NET application that consumes the package is published, the libnode binary for the target runtime-identifier would get copied to the publish output directory. That would be the assembly directory, or at least relative to it. So I'm thinking the best default should probably be just DllImportSearchPath.AssemblyDirectory, and we can let the application override that with an optional parameter as I suggested above.

We are working on setting up that infrastructure for building libnode, packaging multiple platforms in nuget packages, and publishing them. When that is further along, it might be necessary to make adjustments to the loading code here anyway.

src/NodeApi/Runtime/NativeLibrary.cs Show resolved Hide resolved
$"{libraryName}.{libraryExtension}",
$"lib{libraryName}.{libraryExtension}"
];
}
Copy link
Member

Choose a reason for hiding this comment

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

This else block is not needed. This method is only used with .NET Framework 4.x, which only runs on Windows, so there is no need to consider other operating systems.

Copy link
Author

Choose a reason for hiding this comment

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

I was thinking about Mono here - not sure if it's a target you might want to support though.

Copy link
Member

Choose a reason for hiding this comment

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

We're not attempting to support Mono. There are several places in code already that assume anything within #if NETFRAMEWORK is Windows-only.

src/NodeApi/Runtime/NativeLibrary.cs Outdated Show resolved Hide resolved
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.

2 participants