Skip to content

Fix native library resolution when a same-named DLL exists in a PATH directory - #100

Closed
oxygen-dioxide wants to merge 1 commit into
DataficationSDK:mainfrom
oxygen-dioxide:fix-native-dll-hijack
Closed

Fix native library resolution when a same-named DLL exists in a PATH directory#100
oxygen-dioxide wants to merge 1 commit into
DataficationSDK:mainfrom
oxygen-dioxide:fix-native-dll-hijack

Conversation

@oxygen-dioxide

Copy link
Copy Markdown

Summary

Notebooks that load native dependencies via #r "nuget: ..." fail with a native library version mismatch if an unrelated copy of the library happens to live in any directory on the PATH.

[error] System.TypeInitializationException: The type initializer for 'ScottPlot.Fonts' threw an exception.
  ---> System.TypeInitializationException: The type initializer for 'SkiaSharp.SKTypeface' threw an exception.
  ---> System.TypeInitializationException: The type initializer for 'SkiaSharp.SKObject' threw an exception.
  ---> System.InvalidOperationException: The version of the native libSkiaSharp library (80.2) is incompatible with this version of SkiaSharp. Supported versions of the native libSkiaSharp library are in the range [119.0, 120.0).

Issue #91 will be fixed

Minimal reproduction

  1. Create a folder, e.g. C:\native-pollution, and add it to your PATH.
  2. Download libSkiaSharp.dll (80.2) and place it in that folder.
  3. Run the following in a Verso notebook:
#r "nuget:ScottPlot, 5.1.59"
using ScottPlot;

double[] xs = Generate.Consecutive(51);
double[] ys = Generate.Sin(51);
double[] ys2 = Generate.Cos(51);

var plt = new Plot();
plt.Add.Scatter(xs, ys);
plt.Add.Scatter(xs, ys2);
plt.Title("ScottPlot Example: Sine and Cosine");
plt.XLabel("X");
plt.YLabel("Y");

plt.GetSvgHtml(600, 400).Display("text/html")

Before the fix: the run fails with the libSkiaSharp (80.2) is incompatible ... [119.0, 120.0) error shown above.

After the fix: all cells succeed; ScottPlot renders using the 3.119.0 native asset from the NuGet cache.

Control: remove libSkiaSharp.dll from the folder (or drop the folder from PATH) and the notebook works both before and after the fix.

Root cause

Native resolution was handled only through AssemblyLoadContext.Default.ResolvingUnmanagedDll (src/Verso/Kernels/NativeLibraryResolver.cs). Per the .NET unmanaged loading algorithm, that event fires last, only after the runtime's default native probing has failed. On Windows the default probing uses LoadLibrary, whose search includes the directories listed in PATH.

So when a stray libSkiaSharp.dll (e.g. version 80.2 shipped inside some other app's folder that is on PATH) is found by LoadLibrary, the wrong native library is loaded successfully — and Verso's resolver never gets a chance to supply the correct one. The same code works in a csproj because there the matching native asset is copied to the app output directory and is found first by default probing.

How I fixed it

Install a per-assembly NativeLibrary.SetDllImportResolver on every managed assembly loaded from a NuGet package directory. A DllImportResolver is consulted before the default probing, so the native asset extracted from the referenced NuGet package always wins over unrelated copies found through PATH.

The resolver reuses the existing search logic (TryResolveNative), including the same-package-version preference, and is attached both when we load an assembly through the Resolving handler and for any assembly loaded directly from a package directory (tracked via AppDomain.AssemblyLoad). The ResolvingUnmanagedDll handler is kept as a fallback for libraries not found by default probing (e.g. e_sqlite3).

Signed-off-by: unknown <1463567152@qq.com>
@oxygen-dioxide
oxygen-dioxide force-pushed the fix-native-dll-hijack branch from 4e467ee to 0e3818a Compare August 6, 2026 07:35
@TorreyBetts

Copy link
Copy Markdown
Contributor

Thanks for tracking this down. Your diagnosis is right and SetDllImportResolver is the correct hook, so I'd like to merge this, with three changes first.

The main one is scope. Once a resolver is attached, the runtime consults it for every P/Invoke in that assembly, including ones bound for OS libraries, and TryResolveNative then searches every registered native directory. So a package that ships anything under runtimes/{rid}/native/ can end up satisfying an import belonging to an unrelated package, which is close to the problem we're fixing. Could you limit the DllImportResolver path to directories matching the requesting assembly's package version, reusing BelongsToPackageVersion, and return IntPtr.Zero otherwise? A flag on TryResolveNative or a second method would do it. Returning zero falls through to default probing, and OnResolvingUnmanagedDll still runs the wide search last, so ScottPlot keeps working and nothing outside the narrow case changes.

Second, AssembliesWithResolver is keyed on assembly.Location, but resolvers attach per Assembly instance. OnResolvingManagedAssembly loads the same path into IsolationContext when it hits a TPA conflict, and that copy is a different Assembly with an identical location, so it gets skipped and ends up with no resolver. Could you key the set on the assembly itself instead?

Third, the two AttachDllImportResolver calls you added inside OnResolvingManagedAssembly never do anything. AppDomain.AssemblyLoad fires before LoadFromAssemblyPath returns, so the location is already recorded by the time those lines are reached. Both can go back to the original one line form, which keeps the diff smaller.

Last thing, and this one is not mine to waive. The sign-off is a legal attestation rather than a formality. Under the Developer Certificate of Origin you are certifying that you wrote this code or otherwise hold the rights to it, and that you have the right to submit it under the MIT license Verso ships under. That attestation is what establishes the provenance of the intellectual property in the project, and it only carries weight if it is attributable to an identifiable person. The DCO comes from the Linux kernel, which states the rule plainly: your real name, no pseudonyms and no anonymous contributions. CONTRIBUTING.md asks for the same. Your commit currently reads Signed-off-by: unknown <1463567152@qq.com>, with the git author name also set to unknown, so as it stands I can't take the change. Could you set git config user.name to your real name, then run git commit --amend -s --no-edit and force-push? Our check only verifies that a sign-off line is present, which is why it went green, but presence is not the same as a valid certification.

I checked the first three against a small test app rather than going by the docs, so let me know if you want it.

TorreyBetts added a commit that referenced this pull request Aug 6, 2026
Replace locked List<string> search dirs with volatile string[] snapshots to avoid locking during runtime load handlers; writers swap in new arrays via Append. Register an AppDomain.AssemblyLoad handler that attaches a DllImportResolver to assemblies loaded from package directories so package-owned native libraries are preferred ahead of normal probing. Add ResolvePackageNative, TryLoadFrom, IsFromPackageDirectory, IsRegisteredDirectory, Normalize and related helpers, and refactor unmanaged/managed resolution to use the snapshot arrays. Include safety try/catch around assembly-load wiring. Update tests to cover IsRegisteredDirectory behavior.

Diagnosed by [@oxygen-dioxide](https://github.com/oxygen-dioxide) (#91 and #100)

Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
@TorreyBetts

Copy link
Copy Markdown
Contributor

Sorry to see this closed. I reproduced it here against a stray libSkiaSharp that the runtime's own probing finds before the package's own copy, and confirmed that a DllImportResolver is the only hook that runs early enough to beat it.

The fix is in for the next release. You'll be credited for the diagnosis in the release notes. Thanks for continuing to work on this, especially after I closed the issue twice on the wrong theory. If you have something else you'd like to send our way, I'd be glad to look at it.

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