Skip to content

Commit e297470

Browse files
Implement LoadLibraryErrorTracker (#69842)
Finishes the LoadLibraryErrorTracker port from CoreCLR VM to C#. This allows us to report more details about the reason of `DllImport` resolution failures at runtime. The logic matches CoreCLR. There might be a slight behavior difference for empty string literal `DllImport`/`NativeLibrary.Load`. Not sure we care enough (I couldn't discern what the CoreCLR behavior is - I think we report an arbitrary old message from the last load attempt since we never even try to `dlopen` the empty string). Instead of: ``` System.DllNotFoundException: Unable to load native library 'bruh' or one of its dependencies. ``` We can now do: ``` System.DllNotFoundException: Unable to load DLL 'bruh' or one of its dependencies: The process cannot access the file because it is being used by another process. ```
1 parent abca8a1 commit e297470

File tree

11 files changed

+198
-97
lines changed

11 files changed

+198
-97
lines changed

src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@
258258
</ItemGroup>
259259
<ItemGroup Condition="'$(TargetsWindows)'=='true'">
260260
<Compile Include="Microsoft\Win32\SafeHandles\SafeThreadPoolIOHandle.cs" />
261+
<Compile Include="System\Runtime\InteropServices\NativeLibrary.NativeAot.Windows.cs" />
261262
<Compile Include="System\Runtime\InteropServices\PInvokeMarshal.Windows.cs" />
262263
<Compile Include="$(CommonPath)\System\Runtime\InteropServices\Variant.cs">
263264
<Link>System\Runtime\InteropServices\Variant.cs</Link>
@@ -317,6 +318,7 @@
317318
</ItemGroup>
318319
<ItemGroup Condition="'$(TargetsUnix)'=='true'">
319320
<Compile Include="System\Environment.NativeAot.Unix.cs" />
321+
<Compile Include="System\Runtime\InteropServices\NativeLibrary.NativeAot.Unix.cs" />
320322
<Compile Include="System\Runtime\InteropServices\PInvokeMarshal.Unix.cs" />
321323
<Compile Include="System\Threading\LowLevelLifoSemaphore.Unix.cs" />
322324
<Compile Include="System\Threading\Thread.NativeAot.Unix.cs" />
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace System.Runtime.InteropServices
7+
{
8+
public static partial class NativeLibrary
9+
{
10+
private const int LoadWithAlteredSearchPathFlag = 0;
11+
12+
private static IntPtr LoadLibraryHelper(string libraryName, int flags, ref LoadLibErrorTracker errorTracker)
13+
{
14+
// do the Dos/Unix conversion
15+
libraryName = libraryName.Replace('\\', '/');
16+
17+
IntPtr ret = Interop.Sys.LoadLibrary(libraryName);
18+
if (ret == IntPtr.Zero)
19+
{
20+
string? message = Marshal.PtrToStringAnsi(Interop.Sys.GetLoadLibraryError());
21+
errorTracker.TrackErrorMessage(message);
22+
}
23+
24+
return ret;
25+
}
26+
27+
private static void FreeLib(IntPtr handle)
28+
{
29+
Debug.Assert(handle != IntPtr.Zero);
30+
31+
Interop.Sys.FreeLibrary(handle);
32+
}
33+
34+
private static unsafe IntPtr GetSymbolOrNull(IntPtr handle, string symbolName)
35+
{
36+
return Interop.Sys.GetProcAddress(handle, symbolName);
37+
}
38+
39+
internal struct LoadLibErrorTracker
40+
{
41+
private string? _errorMessage;
42+
43+
public void Throw(string libraryName)
44+
{
45+
#if TARGET_OSX
46+
throw new DllNotFoundException(SR.Format(SR.DllNotFound_Mac, libraryName, _errorMessage));
47+
#else
48+
throw new DllNotFoundException(SR.Format(SR.DllNotFound_Linux, libraryName, _errorMessage));
49+
#endif
50+
}
51+
52+
public void TrackErrorMessage(string? message)
53+
{
54+
_errorMessage = message;
55+
}
56+
}
57+
}
58+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace System.Runtime.InteropServices
7+
{
8+
public static partial class NativeLibrary
9+
{
10+
private const int LoadWithAlteredSearchPathFlag = 0x8; /* LOAD_WITH_ALTERED_SEARCH_PATH */
11+
12+
private static IntPtr LoadLibraryHelper(string libraryName, int flags, ref LoadLibErrorTracker errorTracker)
13+
{
14+
IntPtr hmod;
15+
16+
if (((uint)flags & 0xFFFFFF00) != 0)
17+
{
18+
hmod = Interop.Kernel32.LoadLibraryEx(libraryName, IntPtr.Zero, (int)((uint)flags & 0xFFFFFF00));
19+
if (hmod != IntPtr.Zero)
20+
{
21+
return hmod;
22+
}
23+
24+
int lastError = Marshal.GetLastWin32Error();
25+
if (lastError != Interop.Errors.ERROR_INVALID_PARAMETER)
26+
{
27+
errorTracker.TrackErrorCode(lastError);
28+
return hmod;
29+
}
30+
}
31+
32+
hmod = Interop.Kernel32.LoadLibraryEx(libraryName, IntPtr.Zero, flags & 0xFF);
33+
if (hmod == IntPtr.Zero)
34+
{
35+
errorTracker.TrackErrorCode(Marshal.GetLastWin32Error());
36+
}
37+
38+
return hmod;
39+
}
40+
41+
private static void FreeLib(IntPtr handle)
42+
{
43+
Debug.Assert(handle != IntPtr.Zero);
44+
45+
bool result = Interop.Kernel32.FreeLibrary(handle);
46+
if (!result)
47+
throw new InvalidOperationException();
48+
}
49+
50+
private static unsafe IntPtr GetSymbolOrNull(IntPtr handle, string symbolName)
51+
{
52+
return Interop.Kernel32.GetProcAddress(handle, symbolName);
53+
}
54+
55+
// Preserving good error info from DllImport-driven LoadLibrary is tricky because we keep loading from different places
56+
// if earlier loads fail and those later loads obliterate error codes.
57+
//
58+
// This tracker object will keep track of the error code in accordance to priority:
59+
//
60+
// low-priority: unknown error code (should never happen)
61+
// medium-priority: dll not found
62+
// high-priority: dll found but error during loading
63+
//
64+
// We will overwrite the previous load's error code only if the new error code is higher priority.
65+
internal struct LoadLibErrorTracker
66+
{
67+
private int _errorCode;
68+
private int _priority;
69+
70+
private const int PriorityNotFound = 10;
71+
private const int PriorityAccessDenied = 20;
72+
private const int PriorityCouldNotLoad = 99999;
73+
74+
public void Throw(string libraryName)
75+
{
76+
if (_errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT)
77+
{
78+
throw new BadImageFormatException();
79+
}
80+
81+
string message = Interop.Kernel32.GetMessage(_errorCode);
82+
throw new DllNotFoundException(SR.Format(SR.DllNotFound_Windows, libraryName, message));
83+
}
84+
85+
public void TrackErrorCode(int errorCode)
86+
{
87+
int priority = errorCode switch
88+
{
89+
Interop.Errors.ERROR_FILE_NOT_FOUND or
90+
Interop.Errors.ERROR_PATH_NOT_FOUND or
91+
Interop.Errors.ERROR_MOD_NOT_FOUND or
92+
Interop.Errors.ERROR_DLL_NOT_FOUND => PriorityNotFound,
93+
94+
// If we can't access a location, we can't know if the dll's there or if it's good.
95+
// Still, this is probably more unusual (and thus of more interest) than a dll-not-found
96+
// so give it an intermediate priority.
97+
Interop.Errors.ERROR_ACCESS_DENIED => PriorityAccessDenied,
98+
99+
// Assume all others are "dll found but couldn't load."
100+
_ => PriorityCouldNotLoad,
101+
};
102+
103+
if (priority > _priority)
104+
{
105+
_errorCode = errorCode;
106+
_priority = priority;
107+
}
108+
}
109+
}
110+
}
111+
}
Lines changed: 4 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
#nullable enable
5-
using System.Diagnostics;
64
using System.IO;
75
using System.Reflection;
8-
using System.Text;
6+
97
using LibraryNameVariation = System.Runtime.Loader.LibraryNameVariation;
108

119
namespace System.Runtime.InteropServices
@@ -60,7 +58,7 @@ internal static IntPtr LoadBySearch(Assembly callingAssembly, bool searchAssembl
6058
{
6159
IntPtr ret;
6260

63-
int loadWithAlteredPathFlags = 0;
61+
int loadWithAlteredPathFlags = LoadWithAlteredSearchPathFlag;
6462
bool libNameIsRelativePath = !Path.IsPathFullyQualified(libraryName);
6563

6664
// P/Invokes are often declared with variations on the actual library name.
@@ -107,7 +105,7 @@ internal static IntPtr LoadBySearch(Assembly callingAssembly, bool searchAssembl
107105
private static IntPtr LoadFromPath(string libraryName, bool throwOnError)
108106
{
109107
LoadLibErrorTracker errorTracker = default;
110-
IntPtr ret = LoadLibraryHelper(libraryName, 0, ref errorTracker);
108+
IntPtr ret = LoadLibraryHelper(libraryName, LoadWithAlteredSearchPathFlag, ref errorTracker);
111109
if (throwOnError && ret == IntPtr.Zero)
112110
{
113111
errorTracker.Throw(libraryName);
@@ -116,102 +114,13 @@ private static IntPtr LoadFromPath(string libraryName, bool throwOnError)
116114
return ret;
117115
}
118116

119-
private static IntPtr LoadLibraryHelper(string libraryName, int flags, ref LoadLibErrorTracker errorTracker)
120-
{
121-
#if TARGET_WINDOWS
122-
IntPtr ret = Interop.Kernel32.LoadLibraryEx(libraryName, IntPtr.Zero, flags);
123-
if (ret != IntPtr.Zero)
124-
{
125-
return ret;
126-
}
127-
128-
int lastError = Marshal.GetLastWin32Error();
129-
if (lastError != LoadLibErrorTracker.ERROR_INVALID_PARAMETER)
130-
{
131-
errorTracker.TrackErrorCode(lastError);
132-
}
133-
134-
return ret;
135-
#else
136-
IntPtr ret = IntPtr.Zero;
137-
if (libraryName == null)
138-
{
139-
errorTracker.TrackErrorCode(LoadLibErrorTracker.ERROR_MOD_NOT_FOUND);
140-
}
141-
else if (libraryName == string.Empty)
142-
{
143-
errorTracker.TrackErrorCode(LoadLibErrorTracker.ERROR_INVALID_PARAMETER);
144-
}
145-
else
146-
{
147-
// TODO: FileDosToUnixPathA
148-
ret = Interop.Sys.LoadLibrary(libraryName);
149-
if (ret == IntPtr.Zero)
150-
{
151-
errorTracker.TrackErrorCode(LoadLibErrorTracker.ERROR_MOD_NOT_FOUND);
152-
}
153-
}
154-
155-
return ret;
156-
#endif
157-
}
158-
159-
private static void FreeLib(IntPtr handle)
160-
{
161-
Debug.Assert(handle != IntPtr.Zero);
162-
163-
#if !TARGET_UNIX
164-
bool result = Interop.Kernel32.FreeLibrary(handle);
165-
if (!result)
166-
throw new InvalidOperationException();
167-
#else
168-
Interop.Sys.FreeLibrary(handle);
169-
#endif
170-
}
171-
172117
private static unsafe IntPtr GetSymbol(IntPtr handle, string symbolName, bool throwOnError)
173118
{
174-
IntPtr ret;
175-
#if !TARGET_UNIX
176-
var symbolBytes = new byte[Encoding.UTF8.GetByteCount(symbolName) + 1];
177-
Encoding.UTF8.GetBytes(symbolName, symbolBytes);
178-
fixed (byte* pSymbolBytes = symbolBytes)
179-
{
180-
ret = Interop.Kernel32.GetProcAddress(handle, pSymbolBytes);
181-
}
182-
#else
183-
ret = Interop.Sys.GetProcAddress(handle, symbolName);
184-
#endif
119+
IntPtr ret = GetSymbolOrNull(handle, symbolName);
185120
if (throwOnError && ret == IntPtr.Zero)
186121
throw new EntryPointNotFoundException(SR.Format(SR.Arg_EntryPointNotFoundExceptionParameterizedNoLibrary, symbolName));
187122

188123
return ret;
189124
}
190-
191-
// TODO: copy the nice error logic from CoreCLR's LoadLibErrorTracker
192-
// to get fine-grained error messages that take into account access denied, etc.
193-
internal struct LoadLibErrorTracker
194-
{
195-
internal const int ERROR_INVALID_PARAMETER = 0x57;
196-
internal const int ERROR_MOD_NOT_FOUND = 126;
197-
internal const int ERROR_BAD_EXE_FORMAT = 193;
198-
199-
private int _errorCode;
200-
201-
public void Throw(string libraryName)
202-
{
203-
if (_errorCode == ERROR_BAD_EXE_FORMAT)
204-
{
205-
throw new BadImageFormatException();
206-
}
207-
208-
throw new DllNotFoundException(SR.Format(SR.Arg_DllNotFoundExceptionParameterized, libraryName));
209-
}
210-
211-
public void TrackErrorCode(int errorCode)
212-
{
213-
_errorCode = errorCode;
214-
}
215-
}
216125
}
217126
}

src/libraries/Common/src/Interop/Unix/System.Native/Interop.DynamicLoad.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ internal unsafe partial class Sys
1111
[LibraryImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_LoadLibrary", StringMarshalling = StringMarshalling.Utf8)]
1212
internal static partial IntPtr LoadLibrary(string filename);
1313

14+
[LibraryImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetLoadLibraryError")]
15+
internal static partial IntPtr GetLoadLibraryError();
16+
1417
[LibraryImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetProcAddress")]
1518
internal static partial IntPtr GetProcAddress(IntPtr handle, byte* symbol);
1619

src/libraries/Common/src/Interop/Windows/Interop.Errors.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal static partial class Errors
3434
internal const int ERROR_CALL_NOT_IMPLEMENTED = 0x78;
3535
internal const int ERROR_INSUFFICIENT_BUFFER = 0x7A;
3636
internal const int ERROR_INVALID_NAME = 0x7B;
37+
internal const int ERROR_MOD_NOT_FOUND = 0x7E;
3738
internal const int ERROR_NEGATIVE_SEEK = 0x83;
3839
internal const int ERROR_DIR_NOT_EMPTY = 0x91;
3940
internal const int ERROR_BAD_PATHNAME = 0xA1;

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DynamicLoad.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ internal static unsafe partial class Kernel32
1212
{
1313
[LibraryImport(Libraries.Kernel32)]
1414
internal static partial IntPtr GetProcAddress(IntPtr hModule, byte* lpProcName);
15+
16+
[LibraryImport(Libraries.Kernel32, StringMarshalling = StringMarshalling.Utf8)]
17+
internal static partial IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
1518
}
1619
}

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3910,8 +3910,14 @@
39103910
<data name="Arg_EntryPointNotFoundExceptionParameterizedNoLibrary" xml:space="preserve">
39113911
<value>Unable to find an entry point named '{0}' in native library.</value>
39123912
</data>
3913-
<data name="Arg_DllNotFoundExceptionParameterized" xml:space="preserve">
3914-
<value>Unable to load native library '{0}' or one of its dependencies.</value>
3913+
<data name="DllNotFound_Windows" xml:space="preserve">
3914+
<value>Unable to load DLL '{0}' or one of its dependencies: {1}</value>
3915+
</data>
3916+
<data name="DllNotFound_Linux" xml:space="preserve">
3917+
<value>Unable to load shared library '{0}' or one of its dependencies. In order to help diagnose loading problems, consider using a tool like strace. If you're using glibc, consider setting the LD_DEBUG environment variable: {1}</value>
3918+
</data>
3919+
<data name="DllNotFound_Mac" xml:space="preserve">
3920+
<value>Unable to load shared library '{0}' or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable: {1}</value>
39153921
</data>
39163922
<data name="InvalidOperation_ComInteropRequireComWrapperInstance" xml:space="preserve">
39173923
<value>COM Interop requires ComWrapper instance registered for marshalling.</value>

src/native/libs/System.Native/entrypoints.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ static const Entry s_sysNative[] =
234234
DllImportEntry(SystemNative_LowLevelMonitor_TimedWait)
235235
DllImportEntry(SystemNative_LowLevelMonitor_Signal_Release)
236236
DllImportEntry(SystemNative_LoadLibrary)
237+
DllImportEntry(SystemNative_GetLoadLibraryError)
237238
DllImportEntry(SystemNative_GetProcAddress)
238239
DllImportEntry(SystemNative_FreeLibrary)
239240
DllImportEntry(SystemNative_GetDefaultSearchOrderPseudoHandle)

src/native/libs/System.Native/pal_dynamicload.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ void* SystemNative_LoadLibrary(const char* filename)
3838
return dlopen(filename, RTLD_LAZY);
3939
}
4040

41+
void* SystemNative_GetLoadLibraryError(void)
42+
{
43+
return dlerror();
44+
}
45+
4146
void* SystemNative_GetProcAddress(void* handle, const char* symbol)
4247
{
4348
// We're not trying to disambiguate between "symbol was not found" and "symbol found, but

0 commit comments

Comments
 (0)