Skip to content

Remove always searching executable directory for native libraries in single-file applications #115236

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 13 commits into from
May 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/design/features/host-runtime-information.md
Original file line number Diff line number Diff line change
@@ -80,6 +80,10 @@ List of directory paths corresponding to shared store paths and additional probi

### Single-file

`BUNDLE_EXTRACTION_PATH`

**Added in .NET 10** Path to extraction directory, if the single-file bundle extracted any files. This is used by the runtime to search for native libraries associated with bundled managed assemblies.

`BUNDLE_PROBE`

Hex string representation of a function pointer. It is set when running a single-file application. The function is called by the runtime to look for assemblies bundled into the application. The expected signature is defined as `BundleProbeFn` in [`coreclrhost.h`](/src/coreclr/hosts/inc/coreclrhost.h)
5 changes: 4 additions & 1 deletion src/coreclr/binder/assemblybindercommon.cpp
Original file line number Diff line number Diff line change
@@ -858,7 +858,10 @@ namespace BINDER_SPACE
ProbeExtensionResult probeExtensionResult = AssemblyProbeExtension::Probe(assemblyFileName, /* pathIsBundleRelative */ true);
if (probeExtensionResult.IsValid())
{
SString assemblyFilePath(Bundle::AppIsBundle() ? Bundle::AppBundle->BasePath() : SString::Empty());
SString assemblyFilePath;
if (Bundle::AppIsBundle())
assemblyFilePath.SetUTF8(Bundle::AppBundle->BasePath());

assemblyFilePath.Append(assemblyFileName);

hr = GetAssembly(assemblyFilePath,
10 changes: 8 additions & 2 deletions src/coreclr/inc/bundle.h
Original file line number Diff line number Diff line change
@@ -43,8 +43,13 @@ class Bundle
Bundle(LPCSTR bundlePath, BundleProbeFn *probe);
BundleFileLocation Probe(const SString& path, bool pathIsBundleRelative = false) const;

const SString &Path() const { LIMITED_METHOD_CONTRACT; return m_path; }
const SString &BasePath() const { LIMITED_METHOD_CONTRACT; return m_basePath; }
// Paths do not change and should remain valid for the lifetime of the Bundle
const SString& Path() const { LIMITED_METHOD_CONTRACT; return m_path; }
const UTF8* BasePath() const { LIMITED_METHOD_CONTRACT; return m_basePath.GetUTF8(); }

// Extraction path does not change and should remain valid for the lifetime of the Bundle
bool HasExtractedFiles() const { LIMITED_METHOD_CONTRACT; return !m_extractionPath.IsEmpty(); }
const WCHAR* ExtractionPath() const { LIMITED_METHOD_CONTRACT; return m_extractionPath.GetUnicode(); }

static Bundle* AppBundle; // The BundleInfo for the current app, initialized by coreclr_initialize.
static bool AppIsBundle() { LIMITED_METHOD_CONTRACT; return AppBundle != nullptr; }
@@ -53,6 +58,7 @@ class Bundle
private:
SString m_path; // The path to single-file executable
BundleProbeFn *m_probe;
SString m_extractionPath; // The path to the extraction location, if bundle extracted any files

SString m_basePath; // The prefix to denote a path within the bundle
COUNT_T m_basePathLength;
Original file line number Diff line number Diff line change
@@ -64,9 +64,6 @@ The .NET Foundation licenses this file to you under the MIT license.
<TargetTriple Condition="'$(CrossCompileArch)' != '' and '$(_IsAlpineExitCode)' == '0'">$(CrossCompileArch)-alpine-linux-$(CrossCompileAbi)</TargetTriple>
<TargetTriple Condition="'$(CrossCompileArch)' != '' and ($(CrossCompileRid.StartsWith('freebsd')))">$(CrossCompileArch)-unknown-freebsd12</TargetTriple>

<IlcRPath Condition="'$(IlcRPath)' == '' and '$(_IsApplePlatform)' != 'true'">$ORIGIN</IlcRPath>
<IlcRPath Condition="'$(IlcRPath)' == '' and '$(_IsApplePlatform)' == 'true'">@executable_path</IlcRPath>

<SharedLibraryInstallName Condition="'$(SharedLibraryInstallName)' == '' and '$(_IsApplePlatform)' == 'true' and '$(NativeLib)' == 'Shared'">@rpath/$(TargetName)$(NativeBinaryExt)</SharedLibraryInstallName>

<EventPipeName>libeventpipe-disabled</EventPipeName>
@@ -234,8 +231,6 @@ The .NET Foundation licenses this file to you under the MIT license.
<LinkerArg Include="--target=$(TargetTriple)" Condition="'$(TargetTriple)' != ''" />
<LinkerArg Include="-g" Condition="$(NativeDebugSymbols) == 'true'" />
<LinkerArg Include="-Wl,--strip-debug" Condition="$(NativeDebugSymbols) != 'true' and '$(_IsApplePlatform)' != 'true'" />
<LinkerArg Include="-Wl,-rpath,'$(IlcRPath)'" Condition="'$(StaticExecutable)' != 'true' and !$([MSBuild]::IsOSPlatform('Windows'))" />
<LinkerArg Include="-Wl,-rpath,&quot;$(IlcRPath)&quot;" Condition="'$(StaticExecutable)' != 'true' and $([MSBuild]::IsOSPlatform('Windows'))" />
<LinkerArg Include="-Wl,-install_name,&quot;$(SharedLibraryInstallName)&quot;" Condition="'$(_IsApplePlatform)' == 'true' and '$(NativeLib)' == 'Shared'" />
<LinkerArg Include="-Wl,--build-id=sha1" Condition="'$(_IsApplePlatform)' != 'true'" />
<LinkerArg Include="-Wl,--as-needed" Condition="'$(_IsApplePlatform)' != 'true'" />
Original file line number Diff line number Diff line change
@@ -292,7 +292,7 @@ internal static unsafe void FixupModuleCell(ModuleFixupCell* pCell)
{
string moduleName = GetModuleName(pCell);

uint dllImportSearchPath = 0;
uint dllImportSearchPath = (uint)DllImportSearchPath.AssemblyDirectory;
bool hasDllImportSearchPath = (pCell->DllImportSearchPathAndCookie & InteropDataConstants.HasDllImportSearchPath) != 0;
if (hasDllImportSearchPath)
{
2 changes: 1 addition & 1 deletion src/coreclr/vm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -53,7 +53,6 @@ set(VM_SOURCES_DAC_AND_WKS_COMMON
assembly.cpp
assemblybinder.cpp
binder.cpp
bundle.cpp
castcache.cpp
callcounting.cpp
cdacplatformmetadata.cpp
@@ -296,6 +295,7 @@ set(VM_SOURCES_WKS
assemblyprobeextension.cpp
assemblyspec.cpp
baseassemblyspec.cpp
bundle.cpp
${RUNTIME_DIR}/CachedInterfaceDispatch.cpp
CachedInterfaceDispatchCoreclr.cpp
cachelinealloc.cpp
5 changes: 5 additions & 0 deletions src/coreclr/vm/bundle.cpp
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@

#include "common.h"
#include "bundle.h"
#include "hostinformation.h"
#include <utilcode.h>
#include <corhost.h>
#include <sstring.h>
@@ -47,6 +48,10 @@ Bundle::Bundle(LPCSTR bundlePath, BundleProbeFn *probe)
size_t baseLen = pos - bundlePath + 1; // Include DIRECTORY_SEPARATOR_CHAR_A in m_basePath
m_basePath.SetUTF8(bundlePath, (COUNT_T)baseLen);
m_basePathLength = (COUNT_T)baseLen;

SString extractionPathMaybe;
if (HostInformation::GetProperty(HOST_PROPERTY_BUNDLE_EXTRACTION_PATH, extractionPathMaybe))
m_extractionPath.Set(extractionPathMaybe.GetUnicode());
}

BundleFileLocation Bundle::Probe(const SString& path, bool pathIsBundleRelative) const
21 changes: 19 additions & 2 deletions src/coreclr/vm/nativelibrary.cpp
Original file line number Diff line number Diff line change
@@ -472,12 +472,20 @@ namespace
NATIVE_LIBRARY_HANDLE LoadFromPInvokeAssemblyDirectory(Assembly *pAssembly, LPCWSTR libName, DWORD flags, LoadLibErrorTracker *pErrorTracker)
{
STANDARD_VM_CONTRACT;
_ASSERTE(libName != NULL);

if (pAssembly->GetPEAssembly()->GetPath().IsEmpty())
SString path{ pAssembly->GetPEAssembly()->GetPath() };

// Bundled assembly - path will be empty, path to load should point to the single-file bundle
bool isBundledAssembly = pAssembly->GetPEAssembly()->HasPEImage() && pAssembly->GetPEAssembly()->GetPEImage()->IsInBundle();
_ASSERTE(!isBundledAssembly || Bundle::AppBundle != NULL);
if (isBundledAssembly)
path.Set(pAssembly->GetPEAssembly()->GetPEImage()->GetPathToLoad());

if (path.IsEmpty())
return NULL;

NATIVE_LIBRARY_HANDLE hmod = NULL;
SString path{ pAssembly->GetPEAssembly()->GetPath() };
_ASSERTE(!Path::IsRelative(path));

SString::Iterator lastPathSeparatorIter = path.End();
@@ -490,6 +498,15 @@ namespace
hmod = LocalLoadLibraryHelper(path, flags, pErrorTracker);
}

// Bundle with additional files extracted - also treat the extraction path as the assembly directory for native library load
if (hmod == NULL && isBundledAssembly && Bundle::AppBundle->HasExtractedFiles())
{
path.Set(Bundle::AppBundle->ExtractionPath());
path.Append(DIRECTORY_SEPARATOR_CHAR_W);
path.Append(libName);
hmod = LocalLoadLibraryHelper(path, flags, pErrorTracker);
}

return hmod;
}

7 changes: 1 addition & 6 deletions src/coreclr/vm/peimage.cpp
Original file line number Diff line number Diff line change
@@ -758,8 +758,6 @@ PTR_PEImage PEImage::CreateFromHMODULE(HMODULE hMod)
}
#endif // !TARGET_UNIX

#endif //DACCESS_COMPILE

HANDLE PEImage::GetFileHandle()
{
CONTRACTL
@@ -776,11 +774,7 @@ HANDLE PEImage::GetFileHandle()

if (m_hFile == INVALID_HANDLE_VALUE)
{
#if !defined(DACCESS_COMPILE)
EEFileLoadException::Throw(GetPathToLoad(), hr);
#else // defined(DACCESS_COMPILE)
ThrowHR(hr);
#endif // !defined(DACCESS_COMPILE)
}

return m_hFile;
@@ -819,6 +813,7 @@ HRESULT PEImage::TryOpenFile(bool takeLock)
return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
}

#endif // !DACCESS_COMPILE

BOOL PEImage::IsPtrInImage(PTR_CVOID data)
{
2 changes: 2 additions & 0 deletions src/coreclr/vm/peimage.h
Original file line number Diff line number Diff line change
@@ -145,8 +145,10 @@ class PEImage final
INT64 GetSize() const;
BOOL IsCompressed(INT64* uncompressedSize = NULL) const;

#ifndef DACCESS_COMPILE
HANDLE GetFileHandle();
HRESULT TryOpenFile(bool takeLock = false);
#endif

void GetMVID(GUID *pMvid);
BOOL HasV1Metadata();
6 changes: 2 additions & 4 deletions src/installer/tests/AppHost.Bundle.Tests/NativeLibraries.cs
Original file line number Diff line number Diff line change
@@ -50,8 +50,7 @@ private void PInvoke(bool selfContained, bool bundleNative)
.Should().Pass()
.And.CallPInvoke(null, true)
.And.CallPInvoke(DllImportSearchPath.AssemblyDirectory, true)
// Single-file always looks in application directory, even when only System32 is specified
.And.CallPInvoke(DllImportSearchPath.System32, true);
.And.CallPInvoke(DllImportSearchPath.System32, false);
}

[Fact]
@@ -84,8 +83,7 @@ private void TryLoad(bool selfContained, bool bundleNative)
.Should().Pass()
.And.TryLoadLibrary(null, true)
.And.TryLoadLibrary(DllImportSearchPath.AssemblyDirectory, true)
// Single-file always looks in application directory, even when only System32 is specified
.And.TryLoadLibrary(DllImportSearchPath.System32, true);
.And.TryLoadLibrary(DllImportSearchPath.System32, false);
}

internal static string GetLibraryName(DllImportSearchPath? flags)
39 changes: 0 additions & 39 deletions src/installer/tests/AppHost.Bundle.Tests/SingleFileApiTests.cs
Original file line number Diff line number Diff line change
@@ -64,45 +64,6 @@ public void SelfContained_BundleAllContent()
.And.HaveStdOutContaining("System.Console location: " + extractionDir); // System.Console should be from extracted location
}

[Fact]
public void NativeSearchDirectories()
{
string singleFile = sharedTestState.BundledAppPath;
string extractionRoot = sharedTestState.App.GetNewExtractionRootPath();
string bundleDir = Directory.GetParent(singleFile).FullName;

// If we don't extract anything to disk, the extraction dir shouldn't
// appear in the native search dirs.
Command.Create(singleFile, "native_search_dirs")
.CaptureStdErr()
.CaptureStdOut()
.EnvironmentVariable(Constants.BundleExtractBase.EnvironmentVariable, extractionRoot)
.Execute()
.Should().Pass()
.And.HaveStdOutContaining(bundleDir)
.And.NotHaveStdOutContaining(extractionRoot);
}

[Fact]
public void NativeSearchDirectories_WithExtraction()
{
SingleFileTestApp app = sharedTestState.App;
string singleFile = app.Bundle(BundleOptions.BundleNativeBinaries, out Manifest manifest);

string extractionRoot = app.GetNewExtractionRootPath();
string extractionDir = app.GetExtractionDir(extractionRoot, manifest).FullName;
string bundleDir = Directory.GetParent(singleFile).FullName;

Command.Create(singleFile, "native_search_dirs")
.CaptureStdErr()
.CaptureStdOut()
.EnvironmentVariable(Constants.BundleExtractBase.EnvironmentVariable, extractionRoot)
.Execute()
.Should().Pass()
.And.HaveStdOutContaining(extractionDir)
.And.HaveStdOutContaining(bundleDir);
}

public class SharedTestState : IDisposable
{
public SingleFileTestApp App { get; set; }
Original file line number Diff line number Diff line change
@@ -71,11 +71,6 @@ public static int Main(string[] args)
Console.WriteLine("AppContext.BaseDirectory: " + AppContext.BaseDirectory);
break;

case "native_search_dirs":
var native_search_dirs = AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES");
Console.WriteLine("NATIVE_DLL_SEARCH_DIRECTORIES: " + native_search_dirs);
break;

default:
Console.WriteLine("test failure");
return -1;
1 change: 1 addition & 0 deletions src/native/corehost/host_runtime_contract.h
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
#define HOST_PROPERTY_RUNTIME_CONTRACT "HOST_RUNTIME_CONTRACT"
#define HOST_PROPERTY_APP_PATHS "APP_PATHS"
#define HOST_PROPERTY_BUNDLE_PROBE "BUNDLE_PROBE"
#define HOST_PROPERTY_BUNDLE_EXTRACTION_PATH "BUNDLE_EXTRACTION_PATH"
#define HOST_PROPERTY_ENTRY_ASSEMBLY_NAME "ENTRY_ASSEMBLY_NAME"
#define HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES "NATIVE_DLL_SEARCH_DIRECTORIES"
#define HOST_PROPERTY_PINVOKE_OVERRIDE "PINVOKE_OVERRIDE"
13 changes: 0 additions & 13 deletions src/native/corehost/hostpolicy/deps_resolver.cpp
Original file line number Diff line number Diff line change
@@ -854,19 +854,6 @@ bool deps_resolver_t::resolve_probe_dirs(
}
}

// If this is a single-file app, add the app's dir to the native search directories.
if (bundle::info_t::is_single_file_bundle() && !is_resources)
{
auto bundle = bundle::runner_t::app();
add_unique_path(asset_type, bundle->base_path(), &items, output, &non_serviced, core_servicing);

// Add the extraction path if it exists.
if (pal::directory_exists(bundle->extraction_path()))
{
add_unique_path(asset_type, bundle->extraction_path(), &items, output, &non_serviced, core_servicing);
}
}

output->append(non_serviced);

return true;
12 changes: 12 additions & 0 deletions src/native/corehost/hostpolicy/hostpolicy_context.cpp
Original file line number Diff line number Diff line change
@@ -120,6 +120,18 @@ namespace
return pal::pal_utf8string(get_filename_without_ext(context->application), value_buffer, value_buffer_size);
}

if (::strcmp(key, HOST_PROPERTY_BUNDLE_EXTRACTION_PATH) == 0)
{
if (!bundle::info_t::is_single_file_bundle())
return -1;

auto bundle = bundle::runner_t::app();
if (bundle->extraction_path().empty())
return -1;

return pal::pal_utf8string(bundle->extraction_path(), value_buffer, value_buffer_size);
}

// Properties from runtime initialization
pal::string_t key_str;
if (pal::clr_palstring(key, &key_str))
10 changes: 5 additions & 5 deletions src/tests/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -527,11 +527,6 @@
variables is horribly slow (seeing 1.4 seconds on a 11th gen Intel Core i7) -->
<IlcUseEnvironmentalTools>true</IlcUseEnvironmentalTools>

<!-- NativeAOT compiled output is placed into a 'native' subdirectory: we need to tweak
rpath so that the test can load its native library dependencies if there's any -->
<IlcRPath Condition="'$(TargetOS)' == 'osx' or '$(TargetsAppleMobile)' == 'true'">@executable_path/..</IlcRPath>
<IlcRPath Condition="'$(IlcRPath)' == ''">$ORIGIN/..</IlcRPath>

<!-- Works around "Error: Native compilation can run on x64 and arm64 hosts only" -->
<DisableUnsupportedError>true</DisableUnsupportedError>

@@ -576,6 +571,11 @@
<IlcArg Include="--trim:xunit.core" />
<IlcArg Include="--trim:xunit.execution.dotnet" />
<IlcArg Include="--trim:xunit.abstractions" />

<!-- NativeAOT compiled output is placed into a 'native' subdirectory: we need to set
rpath so that the test can load its native library dependencies if there's any -->
<LinkerArg Include="-Wl,-rpath,'@executable_path/..'" Condition="'$(SetIlcRPath)' != 'false' and '$(TargetOS)' != 'win' and ('$(TargetOS)' == 'osx' or '$(TargetsAppleMobile)' == 'true')" />
<LinkerArg Include="-Wl,-rpath,'$ORIGIN/..'" Condition="'$(SetIlcRPath)' != 'false' and '$(TargetOS)' != 'win' and '$(TargetOS)' != 'osx' and '$(TargetsAppleMobile)' != 'true'" />
</ItemGroup>

<Import Project="$(RepositoryEngineeringDir)nativeSanitizers.targets" Condition="'$(TestBuildMode)' == 'nativeaot'" />
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.