diff --git a/docs/design/features/host-runtime-information.md b/docs/design/features/host-runtime-information.md index 5501d8c0469813..f0efce70f60d8b 100644 --- a/docs/design/features/host-runtime-information.md +++ b/docs/design/features/host-runtime-information.md @@ -20,7 +20,22 @@ Hex string representation of a pointer to a [`host_runtime_contract` struct](/sr The `get_runtime_property` function provides key-value string information (like that provided for runtime initialization). This removes the requirement to pre-compute and store all properties. [Existing properties](#well-known-runtime-properties) can be migrated to go through this mechanism, allowing for pay-for-play properties and reducing the cost of properties. -Some existing properties (for example, [probing path properties](#probing-paths)) would benefit from being structured data rather than a single string. The contract can be extended to allow querying for that specific, structured information rather than relying only on strings. For backwards compatibility for existing properties, we would still allow getting the string via `get_runtime_property`, but that would be an on-demand cost. +Some existing properties benefit from being provided as structured data rather than a single string. The contract can expose callbacks for querying this information while continuing to provide the corresponding string through `get_runtime_property` for backwards compatibility. + +### Application assembly paths + +**Added in .NET 12** + +The host provides resolved assembly paths through two callbacks: + +- `get_assembly_names` returns the simple names of all host-resolved assemblies. +- `resolve_assembly_to_path` resolves a simple name to directory and file name components. + +The returned strings are owned by the host and remain valid for the lifetime of the process. The runtime records the assembly names during initialization and resolves each path on demand. + +Both callbacks must be available for the structured representation to be used. If either callback is unavailable or `get_assembly_names` returns `false`, the runtime falls back to the [`TRUSTED_PLATFORM_ASSEMBLIES`](#probing-paths) property. + +For compatibility, `get_runtime_property` returns the path-separated `TRUSTED_PLATFORM_ASSEMBLIES` string on demand. A custom host can explicitly set that property to use the legacy representation instead of the structured callbacks. ## Well-known runtime properties @@ -66,6 +81,8 @@ List of assemblies (paths or names) containing a [`StartupHook`](./host-startup- List of platform and application assembly file paths. Paths are delimited by a [platform-specific path separator](#path-separator). This is used in [managed assembly probing](https://learn.microsoft.com/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing). +**.NET 12 and above** The host provides this information through the [application assembly callbacks](#application-assembly-paths) by default. The string representation remains available through `host_runtime_contract.get_runtime_property` and can still be explicitly supplied by a custom host. + `NATIVE_DLL_SEARCH_DIRECTORIES` List of directory paths to search for unmanaged (native) libraries. Paths are delimited by a [platform-specific path separator](#path-separator). This is used in [unmanaged (native) assembly probing](https://learn.microsoft.com/dotnet/core/dependency-loading/default-probing#unmanaged-native-library-probing). diff --git a/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs index c0dedcb85e50b6..76004b8cc1e2d9 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs @@ -16,6 +16,12 @@ private static bool IsKnownHostProperty(string name) or "PLATFORM_RESOURCE_ROOTS" or "APP_PATHS"; + private static bool TryGetHostPropertyValue(string name, out string? value) + { + value = null; + return TryGetHostPropertyValue(name, new StringHandleOnStack(ref value)); + } + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AppContext_TryGetHostPropertyValue", StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool TryGetHostPropertyValue(string name, StringHandleOnStack retValue); diff --git a/src/coreclr/binder/applicationcontext.cpp b/src/coreclr/binder/applicationcontext.cpp index 89b0bf66f7518f..a3681a1a75e382 100644 --- a/src/coreclr/binder/applicationcontext.cpp +++ b/src/coreclr/binder/applicationcontext.cpp @@ -15,6 +15,7 @@ #include "assemblyhashtraits.hpp" #include "stringarraylist.h" #include "failurecache.hpp" +#include "hostinformation.h" #include "utils.hpp" #include "ex.h" #include "clr/fs/path.h" @@ -91,51 +92,67 @@ namespace BINDER_SPACE // m_pTrustedPlatformAssemblyMap = new SimpleNameToFileNameMap(); - sTrustedPlatformAssemblies.Normalize(); - for (SString::Iterator i = sTrustedPlatformAssemblies.Begin(); i != sTrustedPlatformAssemblies.End(); ) + const char* const* assemblyNames; + size_t assemblyCount; + if (HostInformation::GetAssemblyNames(&assemblyNames, &assemblyCount)) { - SString fileName; - SString simpleName; - HRESULT pathResult = S_OK; - IF_FAIL_GO(pathResult = GetNextTPAPath(sTrustedPlatformAssemblies, i, /*dllOnly*/ false, fileName, simpleName)); - if (pathResult == S_FALSE) + for (size_t i = 0; i < assemblyCount; i++) { - break; - } + StackSString simpleName(SString::Utf8, assemblyNames[i]); + _ASSERT(!simpleName.IsEmpty()); - const SimpleNameToFileNameMapEntry *pExistingEntry = m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode()); - if (pExistingEntry != nullptr) - { - continue; - } + if (m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode()) != nullptr) + continue; - LPWSTR wszSimpleName = nullptr; - if (pExistingEntry == nullptr) - { - wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; + LPWSTR wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; if (wszSimpleName == nullptr) { GO_WITH_HRESULT(E_OUTOFMEMORY); } + wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); + + SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, nullptr }; + m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); } - else + } + else + { + sTrustedPlatformAssemblies.Normalize(); + for (SString::Iterator i = sTrustedPlatformAssemblies.Begin(); i != sTrustedPlatformAssemblies.End(); ) { - wszSimpleName = pExistingEntry->m_wszSimpleName; - } + SString fileName; + SString simpleName; + HRESULT pathResult = S_OK; + IF_FAIL_GO(pathResult = GetNextTPAPath(sTrustedPlatformAssemblies, i, /*dllOnly*/ false, fileName, simpleName)); + if (pathResult == S_FALSE) + { + break; + } - LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1]; - if (wszFileName == nullptr) - { - GO_WITH_HRESULT(E_OUTOFMEMORY); - } - wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode()); + const SimpleNameToFileNameMapEntry *pExistingEntry = m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode()); + if (pExistingEntry != nullptr) + { + continue; + } + + LPWSTR wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; + if (wszSimpleName == nullptr) + { + GO_WITH_HRESULT(E_OUTOFMEMORY); + } + wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); - SimpleNameToFileNameMapEntry mapEntry; - mapEntry.m_wszSimpleName = wszSimpleName; - mapEntry.m_wszILFileName = wszFileName; + LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1]; + if (wszFileName == nullptr) + { + GO_WITH_HRESULT(E_OUTOFMEMORY); + } + wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode()); - m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); + SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, wszFileName }; + m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); + } } // diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index b95cd254953aa8..61a2232d5ca00d 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -13,6 +13,7 @@ #include "common.h" #include "assemblybindercommon.hpp" +#include "hostinformation.h" #include "assemblyname.hpp" #include "assembly.hpp" #include "applicationcontext.hpp" @@ -897,11 +898,21 @@ namespace BINDER_SPACE // Is assembly on TPA list? SimpleNameToFileNameMap * tpaMap = pApplicationContext->GetTpaList(); const SimpleNameToFileNameMapEntry *pTpaEntry = tpaMap->LookupPtr(simpleName.GetUnicode()); + SString fileName; if (pTpaEntry != nullptr) { - _ASSERTE(pTpaEntry->m_wszILFileName != nullptr); - SString fileName(pTpaEntry->m_wszILFileName); + if (pTpaEntry->m_wszILFileName != nullptr) + { + fileName.Set(pTpaEntry->m_wszILFileName); + } + else + { + HostInformation::ResolveAssemblyToPath(pTpaEntry->m_wszSimpleName, fileName); + } + } + if (!fileName.IsEmpty()) + { ReleaseHolder pAssembly; SString getAssemblyDiag; hr = GetAssembly(fileName, @@ -1349,5 +1360,3 @@ BOOL AssemblyBinderCommon::IsValidArchitecture(PEKIND kArchitecture) #endif // !defined(DACCESS_COMPILE) }; - - diff --git a/src/coreclr/binder/inc/applicationcontext.hpp b/src/coreclr/binder/inc/applicationcontext.hpp index 6642e68fd5f047..ae39ba463cb7d0 100644 --- a/src/coreclr/binder/inc/applicationcontext.hpp +++ b/src/coreclr/binder/inc/applicationcontext.hpp @@ -52,13 +52,6 @@ namespace BINDER_SPACE void OnDestructPerEntryCleanupAction(const SimpleNameToFileNameMapEntry & e) { - if (e.m_wszILFileName == nullptr) - { - // Don't delete simple name here since it's a filename only entry and will be cleaned up - // by the SimpleName -> FileName entry which reuses the same filename pointer. - return; - } - if (e.m_wszSimpleName != nullptr) { delete [] e.m_wszSimpleName; diff --git a/src/coreclr/hosts/corerun/corerun.cpp b/src/coreclr/hosts/corerun/corerun.cpp index 49908c2675ac74..059dfd5f64e820 100644 --- a/src/coreclr/hosts/corerun/corerun.cpp +++ b/src/coreclr/hosts/corerun/corerun.cpp @@ -18,6 +18,8 @@ #endif // TARGET_BROWSER #include +#include +#include #if defined(TARGET_UNIX) #include @@ -88,9 +90,10 @@ namespace envvar const char_t* mockHostPolicy = W("MOCK_HOSTPOLICY"); // Variable used to indicate how app assemblies should be provided to the runtime - // - PROPERTY: corerun will pass the paths vias the TRUSTED_PLATFORM_ASSEMBLIES property + // - CALLBACK: corerun will pass assembly names and paths via the host runtime contract + // - PROPERTY: corerun will pass the paths via the TRUSTED_PLATFORM_ASSEMBLIES property // - EXTERNAL: corerun will pass an external assembly probe to the runtime for app assemblies - // - Not set: same as PROPERTY + // - Not set: same as CALLBACK // - The TPA list as a platform delimited list of paths. The same format as the system's PATH env var. const char_t* appAssemblies = W("APP_ASSEMBLIES"); @@ -127,9 +130,9 @@ static void wait_for_debugger() // N.B. It seems that CoreCLR doesn't always use the first instance of an assembly on the TPA list // (for example, ni's may be preferred over il, even if they appear later). Therefore, when building -// the TPA only include the first instance of a simple assembly name to allow users the opportunity to +// the TPA only includes the first instance of a simple assembly name to allow users the opportunity to // override Framework assemblies by placing dlls in %CORE_LIBRARIES%. -static string_t build_tpa(const string_t& core_root, const string_t& core_libraries) +static std::unordered_map build_tpa(const string_t& core_root, const string_t& core_libraries) { static const char_t* const tpa_extensions[] = { @@ -138,8 +141,7 @@ static string_t build_tpa(const string_t& core_root, const string_t& core_librar nullptr }; - std::set name_set; - pal::stringstream_t tpa_list; + std::unordered_map tpa; // Iterate over all extensions. for (const char_t* const* curr_ext = tpa_extensions; *curr_ext != nullptr; ++curr_ext) @@ -154,7 +156,7 @@ static string_t build_tpa(const string_t& core_root, const string_t& core_librar continue; assert(dir.back() == pal::dir_delim); - string_t tmp = pal::build_file_list(dir, ext, [&](const char_t* file) + pal::build_file_list(dir, ext, [&](const char_t* file) { string_t file_local{ file }; @@ -162,16 +164,12 @@ static string_t build_tpa(const string_t& core_root, const string_t& core_librar if (pal::string_ends_with(file_local, ext_len, ext)) file_local = file_local.substr(0, file_local.length() - ext_len); - // Return true if the file is new. - return name_set.insert(file_local).second; + return tpa.emplace(std::move(file_local), dir + file).second; }); - - // Add to the TPA. - tpa_list << tmp; } } - return tpa_list.str(); + return tpa; } static bool try_get_export(pal::mod_t mod, const char* symbol, void** fptr) @@ -253,13 +251,27 @@ static void log_error_info(const char* line) std::fprintf(stderr, "%s\n", line); } +struct host_runtime_contract_assembly_path +{ + const char* directory; + std::string file_name; +}; + +struct host_runtime_contract_context +{ + const configuration* config; + std::vector assembly_names; + std::unordered_set assembly_directories; + std::unordered_map assembly_paths; +}; + size_t HOST_CONTRACT_CALLTYPE get_runtime_property( const char* key, char* value_buffer, size_t value_buffer_size, void* contract_context) { - configuration* config = static_cast(contract_context); + const configuration* config = static_cast(contract_context)->config; if (::strcmp(key, HOST_PROPERTY_ENTRY_ASSEMBLY_NAME) == 0) { @@ -300,6 +312,43 @@ size_t HOST_CONTRACT_CALLTYPE get_runtime_property( return -1; } +static bool HOST_CONTRACT_CALLTYPE get_assembly_names( + const char* const** names, + size_t* count, + void* contract_context) +{ + if (names == nullptr || count == nullptr) + return false; + + host_runtime_contract_context* context = static_cast(contract_context); + if (context->assembly_names.empty()) + return false; + + *names = context->assembly_names.data(); + *count = context->assembly_names.size(); + return true; +} + +static bool HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( + const char* simple_name, + const char** directory, + const char** file_name, + void* contract_context) +{ + if (directory == nullptr || file_name == nullptr) + return false; + + host_runtime_contract_context* context = static_cast(contract_context); + std::unordered_map::const_iterator entry = + context->assembly_paths.find(simple_name); + if (entry == context->assembly_paths.end()) + return false; + + *directory = entry->second.directory; + *file_name = entry->second.file_name.c_str(); + return true; +} + // Paths for external assembly probe static char* s_core_libs_path = nullptr; static char* s_core_root_path = nullptr; @@ -452,25 +501,36 @@ static int run(const configuration& config) native_search_dirs << core_root << pal::env_path_delim; } - string_t tpa_list; + std::unordered_map tpa; + string_t tpa_property; string_t app_assemblies_env = pal::getenv(envvar::appAssemblies); + bool use_tpa_callbacks = app_assemblies_env.empty() || app_assemblies_env == W("CALLBACK"); bool use_external_assembly_probe = false; #ifdef TARGET_BROWSER use_external_assembly_probe = true; + use_tpa_callbacks = false; #endif // TARGET_BROWSER - if (app_assemblies_env.empty() || app_assemblies_env == W("PROPERTY")) + if (use_tpa_callbacks) + { + tpa = build_tpa(core_root, core_libs); + } + else if (app_assemblies_env.empty() || app_assemblies_env == W("PROPERTY")) { // Use the TRUSTED_PLATFORM_ASSEMBLIES property to pass the app assemblies to the runtime. - tpa_list = build_tpa(core_root, core_libs); + pal::stringstream_t tpa_list; + for (const std::pair& entry : build_tpa(core_root, core_libs)) + tpa_list << entry.second << pal::env_path_delim; + + tpa_property = tpa_list.str(); } else if (app_assemblies_env == W("EXTERNAL")) { // Use the external assembly probe to load assemblies from the app assembly paths. use_external_assembly_probe = true; } - else + else if (!app_assemblies_env.empty()) { - tpa_list = std::move(app_assemblies_env); + tpa_property = std::move(app_assemblies_env); } if (use_external_assembly_probe) @@ -524,7 +584,7 @@ static int run(const configuration& config) (void)try_get_export(coreclr_mod, "coreclr_set_error_writer", (void**)&coreclr_set_error_writer_func); // Construct CoreCLR properties. - pal::string_utf8_t tpa_list_utf8 = pal::convert_to_utf8(tpa_list.c_str()); + pal::string_utf8_t tpa_property_utf8 = pal::convert_to_utf8(tpa_property.c_str()); pal::string_utf8_t app_path_utf8 = pal::convert_to_utf8(app_path.c_str()); pal::string_utf8_t native_search_dirs_utf8 = pal::convert_to_utf8(native_search_dirs.str().c_str()); @@ -539,10 +599,13 @@ static int run(const configuration& config) std::vector propertyKeys; std::vector propertyValues; - // TRUSTED_PLATFORM_ASSEMBLIES - // - The list of complete paths to each of the fully trusted assemblies - propertyKeys.push_back("TRUSTED_PLATFORM_ASSEMBLIES"); - propertyValues.push_back(tpa_list_utf8.c_str()); + if (!use_tpa_callbacks) + { + // TRUSTED_PLATFORM_ASSEMBLIES + // - The list of complete paths to each of the fully trusted assemblies + propertyKeys.push_back(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES); + propertyValues.push_back(tpa_property_utf8.c_str()); + } // APP_PATHS // - The list of paths which will be probed by the assembly loader @@ -563,14 +626,47 @@ static int run(const configuration& config) for (const pal::string_utf8_t& str : user_defined_values_utf8) propertyValues.push_back(str.c_str()); + host_runtime_contract_context contract_context{ &config }; + if (use_tpa_callbacks) + { + contract_context.assembly_names.reserve(tpa.size()); + contract_context.assembly_directories.reserve(tpa.size()); + contract_context.assembly_paths.reserve(tpa.size()); + for (const std::pair& entry : tpa) + { + string_t directory; + string_t file_name; + pal::split_path_to_dir_filename(entry.second, directory, file_name); + pal::ensure_trailing_delimiter(directory); + + pal::string_utf8_t name = pal::convert_to_utf8(entry.first.c_str()); + pal::string_utf8_t directory_utf8 = pal::convert_to_utf8(directory.c_str()); + pal::string_utf8_t file_name_utf8 = pal::convert_to_utf8(file_name.c_str()); + std::pair::iterator, bool> directory_result = + contract_context.assembly_directories.emplace(directory_utf8.c_str()); + host_runtime_contract_assembly_path path{ + directory_result.first->c_str(), + file_name_utf8.c_str() + }; + std::pair::iterator, bool> result = + contract_context.assembly_paths.emplace(name.c_str(), std::move(path)); + if (result.second) + contract_context.assembly_names.push_back(result.first->first.c_str()); + } + + std::unordered_map().swap(tpa); + } + host_runtime_contract host_contract = { sizeof(host_runtime_contract), - (void*)&config, + &contract_context, &get_runtime_property, nullptr, nullptr, use_external_assembly_probe ? &external_assembly_probe : nullptr, - pal::getenv(envvar::platformNativeR2R) == W("1") ? &get_native_code_data : nullptr }; + pal::getenv(envvar::platformNativeR2R) == W("1") ? &get_native_code_data : nullptr, + &get_assembly_names, + &resolve_assembly_to_path }; propertyKeys.push_back(HOST_PROPERTY_RUNTIME_CONTRACT); std::stringstream ss; ss << "0x" << std::hex << (size_t)(&host_contract); diff --git a/src/coreclr/hosts/corerun/corerun.hpp b/src/coreclr/hosts/corerun/corerun.hpp index e15be76720e466..0cfdd44be9620f 100644 --- a/src/coreclr/hosts/corerun/corerun.hpp +++ b/src/coreclr/hosts/corerun/corerun.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/src/coreclr/inc/hostinformation.h b/src/coreclr/inc/hostinformation.h index cf41ed5f4fdaf3..9784cefab661ef 100644 --- a/src/coreclr/inc/hostinformation.h +++ b/src/coreclr/inc/hostinformation.h @@ -12,6 +12,9 @@ class HostInformation static void SetContract(_In_ host_runtime_contract* hostContract); static bool GetProperty(_In_z_ const char* name, SString& value); + static bool GetAssemblyNames(_Outptr_result_buffer_(*count) const char* const** names, _Out_ size_t* count); + static void ResolveAssemblyToPath(_In_z_ LPCWSTR simpleName, SString& path); + static bool HasExternalProbe(); static bool ExternalAssemblyProbe(_In_ const SString& path, _Out_ void** data, _Out_ int64_t* size); diff --git a/src/coreclr/vm/appdomainnative.cpp b/src/coreclr/vm/appdomainnative.cpp index ffcf604e4e5280..c51484c21916f6 100644 --- a/src/coreclr/vm/appdomainnative.cpp +++ b/src/coreclr/vm/appdomainnative.cpp @@ -13,6 +13,7 @@ #include "../binder/inc/defaultassemblybinder.h" #include "../binder/inc/applicationcontext.hpp" #include +#include "hostinformation.h" #include "stringarraylist.h" // static @@ -165,12 +166,27 @@ extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall BINDER_SPACE::SimpleNameToFileNameMap::Iterator end = pMap->End(); while (i != end) { + SString path; if (i->m_wszILFileName != NULL) + { + path.Set(i->m_wszILFileName); + } + else + { + HostInformation::ResolveAssemblyToPath(i->m_wszSimpleName, path); + if (path.IsEmpty()) + { + ++i; + continue; + } + } + + if (!path.IsEmpty()) { if (!result.IsEmpty()) result.Append(PATH_SEPARATOR_CHAR_W); - result.Append(i->m_wszILFileName); + result.Append(path); } ++i; diff --git a/src/coreclr/vm/hostinformation.cpp b/src/coreclr/vm/hostinformation.cpp index d1f6e1b925a492..65c82d3c3487a8 100644 --- a/src/coreclr/vm/hostinformation.cpp +++ b/src/coreclr/vm/hostinformation.cpp @@ -13,8 +13,7 @@ void HostInformation::SetContract(_In_ host_runtime_contract* hostContract) { _ASSERTE(s_hostContract.size == 0 && hostContract != nullptr); - // Copy the contract values - s_hostContract = *hostContract; + memcpy(&s_hostContract, hostContract, min(hostContract->size, sizeof(s_hostContract))); } bool HostInformation::GetProperty(_In_z_ const char* name, SString& value) @@ -57,6 +56,49 @@ bool HostInformation::GetProperty(_In_z_ const char* name, SString& value) return true; } +bool HostInformation::GetAssemblyNames(const char* const** names, size_t* count) +{ + _ASSERTE(names != nullptr && count != nullptr); + + size_t requiredSize = offsetof(host_runtime_contract, resolve_assembly_to_path) + sizeof(s_hostContract.resolve_assembly_to_path); + if (s_hostContract.size < requiredSize + || s_hostContract.get_assembly_names == nullptr + || s_hostContract.resolve_assembly_to_path == nullptr) + return false; + + if (!s_hostContract.get_assembly_names(names, count, s_hostContract.context)) + return false; + + if (*count != 0 && *names == nullptr) + return false; + + return true; +} + +void HostInformation::ResolveAssemblyToPath(_In_z_ LPCWSTR simpleName, SString& path) +{ + size_t requiredSize = offsetof(host_runtime_contract, resolve_assembly_to_path) + sizeof(s_hostContract.resolve_assembly_to_path); + if (s_hostContract.size < requiredSize || s_hostContract.resolve_assembly_to_path == nullptr) + return; + + StackSString utf8Name; + utf8Name.SetAndConvertToUTF8(simpleName); + const char* directory; + const char* fileName; + if (!s_hostContract.resolve_assembly_to_path(utf8Name.GetUTF8(), &directory, &fileName, s_hostContract.context)) + return; + + if (directory == nullptr || directory[0] == '\0' || fileName == nullptr || fileName[0] == '\0') + return; + + path.SetUTF8(directory); + size_t directoryLength = strlen(directory); + if (directory[directoryLength - 1] != DIRECTORY_SEPARATOR_CHAR_A) + path.Append(DIRECTORY_SEPARATOR_CHAR_W); + + path.AppendUTF8(fileName); +} + bool HostInformation::HasExternalProbe() { size_t requiredSize = offsetof(host_runtime_contract, external_assembly_probe) + sizeof(s_hostContract.external_assembly_probe); diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs index 8894f3d7da114a..4694abfc2b6f30 100644 --- a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs +++ b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs @@ -20,6 +20,8 @@ internal struct host_runtime_contract public IntPtr pinvoke_override; public delegate* unmanaged[Stdcall] external_assembly_probe; public delegate* unmanaged[Stdcall] get_native_code_data; + public delegate* unmanaged[Stdcall] get_assembly_names; + public delegate* unmanaged[Stdcall] resolve_assembly_to_path; } #pragma warning restore CS0649 diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs index ed3335d00e4fc5..a2e9fc5681afb5 100644 --- a/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs +++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs @@ -12,7 +12,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution public static class DependencyResolutionCommandResultExtensions { // App asset resolution extensions - private const string TRUSTED_PLATFORM_ASSEMBLIES = nameof(TRUSTED_PLATFORM_ASSEMBLIES); private const string NATIVE_DLL_SEARCH_DIRECTORIES = nameof(NATIVE_DLL_SEARCH_DIRECTORIES); private const string PLATFORM_RESOURCE_ROOTS = nameof(PLATFORM_RESOURCE_ROOTS); @@ -50,12 +49,41 @@ public static AndConstraint NotHaveRuntimePropertyConta public static AndConstraint HaveResolvedAssembly(this CommandResultAssertions assertion, string assemblyPath, TestApp app = null) { - return assertion.HaveRuntimePropertyContaining(TRUSTED_PLATFORM_ASSEMBLIES, RelativePathsToAbsoluteAppPaths(assemblyPath, app)); + return assertion.ResolvedTpaContains(RelativePathsToAbsoluteAppPaths(assemblyPath, app), expected: true); } public static AndConstraint NotHaveResolvedAssembly(this CommandResultAssertions assertion, string assemblyPath, TestApp app = null) { - return assertion.NotHaveRuntimePropertyContaining(TRUSTED_PLATFORM_ASSEMBLIES, RelativePathsToAbsoluteAppPaths(assemblyPath, app)); + return assertion.ResolvedTpaContains(RelativePathsToAbsoluteAppPaths(assemblyPath, app), expected: false); + } + + private static AndConstraint ResolvedTpaContains( + this CommandResultAssertions assertion, + string[] values, + bool expected) + { + AssertionChain assertionChain = AssertionChain.GetOrCreate(); + + foreach (string value in values) + { + bool found = false; + foreach (string line in assertion.Result.StdErr.Split(Environment.NewLine)) + { + if (line.Contains("TPA entry ", StringComparison.Ordinal) + && line.EndsWith($" = {value}", StringComparison.Ordinal)) + { + found = true; + break; + } + } + + assertionChain.ForCondition(found == expected) + .FailWith(expected + ? $"Resolved assemblies don't contain expected value: '{value}'{assertion.GetDiagnosticsInfo()}" + : $"Resolved assemblies contain unexpected value: '{value}'{assertion.GetDiagnosticsInfo()}"); + } + + return new AndConstraint(assertion); } public static AndConstraint HaveResolvedNativeLibraryPath(this CommandResultAssertions assertion, string path, TestApp app = null) diff --git a/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs b/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs index f3fa0fe3b53023..78afb1377ecf4f 100644 --- a/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs +++ b/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs @@ -273,13 +273,25 @@ public void RuntimeConfig_FilePath_Breaks_MAX_PATH_Threshold() } [Fact] - public void ComputedTPA_NoTrailingPathSeparator() + public void AppDirectoryContainsPathSeparator() { - HostTestContext.BuiltDotNet.Exec(sharedTestState.App.AppDll) - .EnableTracingAndCaptureOutputs() + // TPA paths going through the runtime property string cannot handle a path with the path separator character. + // Going through the host contract (.NET 12+), a path with the path separator character should work properly. + TestApp app = sharedTestState.App.Copy(); + string appDirectory = Path.Combine(app.Location, $"path{Path.PathSeparator}separator"); + Directory.CreateDirectory(appDirectory); + foreach (string file in Directory.GetFiles(app.Location, "*.*", SearchOption.TopDirectoryOnly)) + { + File.Copy(file, Path.Combine(appDirectory, Path.GetFileName(file))); + } + + Command.Create(Path.Combine(appDirectory, Path.GetFileName(app.AppExe))) + .DotNetRoot(HostTestContext.BuiltDotNet.BinPath, HostTestContext.BuildArchitecture) + .CaptureStdOut() + .CaptureStdErr() .Execute() .Should().Pass() - .And.HaveStdErrMatching($"Property TRUSTED_PLATFORM_ASSEMBLIES = .*[^{Path.PathSeparator}]$", System.Text.RegularExpressions.RegexOptions.Multiline); + .And.HaveStdOutContaining("Hello World"); } [Theory] diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/HostContext.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/HostContext.cs index 20cf5f2b749dfb..18da549b0e020a 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/HostContext.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/HostContext.cs @@ -153,6 +153,50 @@ public void RunApp(CommandLine commandLine, bool isSelfContained, string checkPr propertyValidation.ValidateActiveContext(result, newPropertyName); } + [Fact] + public void GetTrustedPlatformAssemblies() + { + const string propertyName = "TRUSTED_PLATFORM_ASSEMBLIES"; + string[] args = + { + HostContextArg, + Scenario.App, + CheckProperties.Get, + sharedState.HostFxrPath, + sharedState.AppPath, + propertyName + }; + + CommandResult result = sharedState.CreateNativeHostCommand(args, sharedState.DotNetRoot) + .Execute(); + + result.Should().Pass() + .And.GetRuntimePropertyValueContaining(LogPrefix.App, propertyName, sharedState.AppPath); + } + + [Fact] + public void SetTrustedPlatformAssemblies() + { + const string propertyName = "TRUSTED_PLATFORM_ASSEMBLIES"; + string[] args = + { + HostContextArg, + Scenario.App, + CheckProperties.Set, + sharedState.HostFxrPath, + sharedState.AppPath, + propertyName + }; + + CommandResult result = sharedState.CreateNativeHostCommand(args, sharedState.DotNetRoot) + .Execute(); + + result.Should().Pass() + .And.SetRuntimePropertyValue(LogPrefix.App, propertyName) + .And.HavePropertyMock(propertyName, PropertyValueFromHost) + .And.HaveStdOutContaining("mock host_runtime_contract[get_assembly_names] = 0"); + } + [Theory] [InlineData(CheckProperties.None)] [InlineData(CheckProperties.Get)] diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/HostContextResultExtensions.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/HostContextResultExtensions.cs index 4a4e55db762a80..31421dc3e94543 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/HostContextResultExtensions.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/HostContextResultExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.RegularExpressions; + using FluentAssertions; namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting @@ -86,6 +88,13 @@ public static AndConstraint GetRuntimePropertyValue(thi return assertion.HaveStdOutContaining($"{prefix}hostfxr_get_runtime_property_value succeeded for property: {name}={value}"); } + public static AndConstraint GetRuntimePropertyValueContaining(this CommandResultAssertions assertion, string prefix, string name, string value) + { + return assertion.HaveStdOutMatching( + $"{Regex.Escape(prefix)}hostfxr_get_runtime_property_value succeeded for property: " + + $"{Regex.Escape(name)}=[^\\r\\n]*{Regex.Escape(value)}"); + } + public static AndConstraint FailToGetRuntimePropertyValue(this CommandResultAssertions assertion, string prefix, string name, int errorCode) { return assertion.HaveStdOutContaining($"{prefix}hostfxr_get_runtime_property_value failed for property: {name} - 0x{errorCode.ToString("x")}"); diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs index d6ec02150e2a49..1e7613a3164804 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs @@ -49,11 +49,10 @@ public static partial class AppContext return data; } -#if !MONO && !NATIVEAOT +#if !NATIVEAOT if (IsKnownHostProperty(name)) { - string? value = null; - if (TryGetHostPropertyValue(name, new StringHandleOnStack(ref value))) + if (TryGetHostPropertyValue(name, out string? value)) { lock (s_dataStore) { @@ -231,6 +230,12 @@ public static void SetSwitch(string switchName, bool isEnabled) } #if MONO + private static bool IsKnownHostProperty(string name) + => name == "TRUSTED_PLATFORM_ASSEMBLIES"; + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern bool TryGetHostPropertyValue(string name, out string? value); + internal static unsafe void Setup(char** pNames, uint* pNameLengths, char** pValues, uint* pValueLengths, int count) { Debug.Assert(s_dataStore == null, "s_dataStore is not expected to be inited before Setup is called"); diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 69dfe4f761eada..4c5423988f24ee 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -117,6 +117,9 @@ ICALL_TYPE(SAFESTRMARSHAL, "Mono.SafeStringMarshal", SAFESTRMARSHAL_1) NOHANDLES(ICALL(SAFESTRMARSHAL_1, "GFree", ves_icall_Mono_SafeStringMarshal_GFree)) NOHANDLES(ICALL(SAFESTRMARSHAL_2, "StringToUtf8_icall", ves_icall_Mono_SafeStringMarshal_StringToUtf8)) +ICALL_TYPE(APPCONTEXT, "System.AppContext", APPCONTEXT_1) +HANDLES(APPCONTEXT_1, "TryGetHostPropertyValue", ves_icall_System_AppContext_TryGetHostPropertyValue, MonoBoolean, 2, (MonoString, MonoStringOut)) + ICALL_TYPE(ARGI, "System.ArgIterator", ARGI_1) NOHANDLES(ICALL(ARGI_1, "IntGetNextArg", ves_icall_System_ArgIterator_IntGetNextArg)) NOHANDLES(ICALL(ARGI_2, "IntGetNextArgType", ves_icall_System_ArgIterator_IntGetNextArgType)) diff --git a/src/mono/mono/mini/CMakeLists.txt b/src/mono/mono/mini/CMakeLists.txt index 33623aae258757..bf23ed044ef0ad 100644 --- a/src/mono/mono/mini/CMakeLists.txt +++ b/src/mono/mono/mini/CMakeLists.txt @@ -119,6 +119,8 @@ set(mini_common_sources interp-stubs.c aot-runtime.h ee.h + hostinformation.h + hostinformation.c mini-runtime.h llvmonly-runtime.h llvmonly-runtime.c diff --git a/src/mono/mono/mini/hostinformation.c b/src/mono/mono/mini/hostinformation.c new file mode 100644 index 00000000000000..cf782cc3175ffc --- /dev/null +++ b/src/mono/mono/mini/hostinformation.c @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include + +#include "hostinformation.h" + +static struct host_runtime_contract host_contract; + +#define HOST_CONTRACT_HAS_FIELD(field) \ + (host_contract.size >= offsetof (struct host_runtime_contract, field) + sizeof (host_contract.field)) + +void +mono_host_information_set_contract (const struct host_runtime_contract *contract) +{ + g_assert (contract != NULL); + g_assert (host_contract.size == 0); + + memcpy (&host_contract, contract, MIN (contract->size, sizeof (host_contract))); +} + +gboolean +mono_host_information_get_assembly_names (const char * const **names, size_t *count) +{ + if (!HOST_CONTRACT_HAS_FIELD (resolve_assembly_to_path) || + host_contract.get_assembly_names == NULL || + host_contract.resolve_assembly_to_path == NULL) + return FALSE; + + return host_contract.get_assembly_names (names, count, host_contract.context); +} + +gboolean +mono_host_information_resolve_assembly_to_path ( + const char *simple_name, + const char **directory, + const char **file_name) +{ + if (directory == NULL || file_name == NULL) + return FALSE; + + *directory = NULL; + *file_name = NULL; + if (!HOST_CONTRACT_HAS_FIELD (resolve_assembly_to_path) || host_contract.resolve_assembly_to_path == NULL) + return FALSE; + + return host_contract.resolve_assembly_to_path (simple_name, directory, file_name, host_contract.context) + && *directory != NULL + && (*directory) [0] != '\0' + && *file_name != NULL + && (*file_name) [0] != '\0'; +} diff --git a/src/mono/mono/mini/hostinformation.h b/src/mono/mono/mini/hostinformation.h new file mode 100644 index 00000000000000..20c57e3fe2ad24 --- /dev/null +++ b/src/mono/mono/mini/hostinformation.h @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __MONO_MINI_HOSTINFORMATION_H__ +#define __MONO_MINI_HOSTINFORMATION_H__ + +#include + +#include +#include + +#include + +void +mono_host_information_set_contract (const struct host_runtime_contract *contract); + +gboolean +mono_host_information_get_assembly_names (const char * const **names, size_t *count); + +gboolean +mono_host_information_resolve_assembly_to_path ( + const char *simple_name, + const char **directory, + const char **file_name); + +#endif /* __MONO_MINI_HOSTINFORMATION_H__ */ diff --git a/src/mono/mono/mini/monovm.c b/src/mono/mono/mini/monovm.c index 9d9b1ae399b022..b2ee98ca5b1ee8 100644 --- a/src/mono/mono/mini/monovm.c +++ b/src/mono/mono/mini/monovm.c @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -73,6 +75,29 @@ parse_trusted_platform_assemblies (const char *assemblies_paths) return TRUE; } +static gboolean +parse_trusted_platform_assemblies_from_contract (void) +{ + const char * const *names = NULL; + size_t count = 0; + if (!mono_host_information_get_assembly_names (&names, &count) || + (count != 0 && names == NULL) || + count >= G_MAXUINT32) + return FALSE; + + MonoCoreTrustedPlatformAssemblies *a = g_new0 (MonoCoreTrustedPlatformAssemblies, 1); + a->assembly_count = (uint32_t)count; + a->basenames = g_new0 (char*, count + 1); + a->basename_lens = g_new0 (uint32_t, count + 1); + for (size_t i = 0; i < count; ++i) { + a->basenames [i] = g_strdup (names [i]); + a->basename_lens [i] = (uint32_t)strlen (a->basenames [i]); + } + + trusted_platform_assemblies = a; + return TRUE; +} + static MonoCoreLookupPaths * parse_lookup_paths (const char *search_path) { @@ -115,15 +140,29 @@ mono_core_preload_hook (MonoAssemblyLoadContext *alc, MonoAssemblyName *aname, c size_t basename_len; basename_len = strlen (basename); + size_t simple_name_len = strlen (aname->name); + gboolean has_fullpaths = a->assembly_filepaths != NULL; for (guint32 i = 0; i < a->assembly_count; ++i) { - if (basename_len == a->basename_lens [i] && !g_strncasecmp (basename, a->basenames [i], a->basename_lens [i])) { + // Host-resolved entries store simple names, while path-based entries store filenames with extensions. + const char *requested_name = has_fullpaths ? basename : aname->name; + size_t requested_name_len = has_fullpaths ? basename_len : simple_name_len; + if (requested_name_len == a->basename_lens [i] && !g_strncasecmp (requested_name, a->basenames [i], a->basename_lens [i])) { MonoAssemblyOpenRequest req; mono_assembly_request_prepare_open (&req, default_alc); req.request.predicate = predicate; req.request.predicate_ud = predicate_ud; - const char *fullpath = a->assembly_filepaths [i]; + char *resolved_path = NULL; + const char *fullpath = has_fullpaths ? a->assembly_filepaths [i] : NULL; + if (!has_fullpaths) { + const char *directory; + const char *file_name; + if (mono_host_information_resolve_assembly_to_path (a->basenames [i], &directory, &file_name)) + fullpath = resolved_path = g_build_filename (directory, file_name, (const char*)NULL); + } + if (fullpath == NULL) + break; gboolean found = g_file_test (fullpath, G_FILE_TEST_IS_REGULAR); @@ -131,15 +170,15 @@ mono_core_preload_hook (MonoAssemblyLoadContext *alc, MonoAssemblyName *aname, c MonoImageOpenStatus status; result = mono_assembly_request_open (fullpath, &req, &status); /* TODO: do something with the status at the end? */ - if (result) - break; } #ifdef ENABLE_WEBCIL else { /* /path/foo.dll -> /path/foo.webcil */ size_t n = strlen (fullpath); - if (n < strlen(".dll")) + if (n < strlen(".dll")) { + g_free (resolved_path); continue; + } n -= strlen(".dll"); char *fullpath2 = g_malloc (n + strlen(".webcil") + 1); g_strlcpy (fullpath2, fullpath, n + 1); @@ -149,20 +188,21 @@ mono_core_preload_hook (MonoAssemblyLoadContext *alc, MonoAssemblyName *aname, c result = mono_assembly_request_open (fullpath2, &req, &status); } g_free (fullpath2); - if (result) - break; - char *fullpath3 = g_malloc (n + strlen(MONO_WEBCIL_IN_WASM_EXTENSION) + 1); - g_strlcpy (fullpath3, fullpath, n + 1); - g_strlcpy (fullpath3 + n, MONO_WEBCIL_IN_WASM_EXTENSION, strlen(MONO_WEBCIL_IN_WASM_EXTENSION) + 1); - if (g_file_test (fullpath3, G_FILE_TEST_IS_REGULAR)) { - MonoImageOpenStatus status; - result = mono_assembly_request_open (fullpath3, &req, &status); + if (!result) { + char *fullpath3 = g_malloc (n + strlen(MONO_WEBCIL_IN_WASM_EXTENSION) + 1); + g_strlcpy (fullpath3, fullpath, n + 1); + g_strlcpy (fullpath3 + n, MONO_WEBCIL_IN_WASM_EXTENSION, strlen(MONO_WEBCIL_IN_WASM_EXTENSION) + 1); + if (g_file_test (fullpath3, G_FILE_TEST_IS_REGULAR)) { + MonoImageOpenStatus status; + result = mono_assembly_request_open (fullpath3, &req, &status); + } + g_free (fullpath3); } - g_free (fullpath3); - if (result) - break; } #endif + g_free (resolved_path); + if (result) + break; } } @@ -183,17 +223,58 @@ install_assembly_loader_hooks (void) mono_install_assembly_preload_hook_v2 (mono_core_preload_hook, (void*)trusted_platform_assemblies, FALSE); } +MonoBoolean +ves_icall_System_AppContext_TryGetHostPropertyValue (MonoStringHandle name, MonoStringHandleOut value, MonoError *error) +{ + MONO_HANDLE_ASSIGN (value, NULL_HANDLE_STRING); + + char *name_utf8 = mono_string_handle_to_utf8 (name, error); + return_val_if_nok (error, FALSE); + gboolean is_tpa = !strcmp (name_utf8, HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES); + g_free (name_utf8); + if (!is_tpa || trusted_platform_assemblies == NULL) + return FALSE; + + GString *property_value = g_string_new (NULL); + for (guint32 i = 0; i < trusted_platform_assemblies->assembly_count; ++i) { + const char *directory; + const char *file_name; + if (!mono_host_information_resolve_assembly_to_path ( + trusted_platform_assemblies->basenames [i], + &directory, + &file_name)) + continue; + + if (property_value->len != 0) + g_string_append_c (property_value, G_SEARCHPATH_SEPARATOR); + char *path = g_build_filename (directory, file_name, (const char*)NULL); + g_string_append (property_value, path); + g_free (path); + } + + if (property_value->len == 0) { + g_string_free (property_value, TRUE); + return FALSE; + } + + MonoStringHandle result = mono_string_new_handle (property_value->str, error); + g_string_free (property_value, TRUE); + return_val_if_nok (error, FALSE); + MONO_HANDLE_ASSIGN (value, result); + return TRUE; +} + static gboolean parse_properties (int propertyCount, const char **propertyKeys, const char **propertyValues) { // A partial list of relevant properties is at: // https://learn.microsoft.com/dotnet/core/tutorials/netcore-hosting#step-3---prepare-runtime-properties - PInvokeOverrideFn override_fn = NULL; + const char *tpa_property = NULL; for (int i = 0; i < propertyCount; ++i) { size_t prop_len = strlen (propertyKeys [i]); if (prop_len == 27 && !strncmp (propertyKeys [i], HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES, 27)) { - parse_trusted_platform_assemblies (propertyValues[i]); + tpa_property = propertyValues [i]; } else if (prop_len == 9 && !strncmp (propertyKeys [i], HOST_PROPERTY_APP_PATHS, 9)) { app_paths = parse_lookup_paths (propertyValues [i]); } else if (prop_len == 23 && !strncmp (propertyKeys [i], HOST_PROPERTY_PLATFORM_RESOURCE_ROOTS, 23)) { @@ -208,6 +289,7 @@ parse_properties (int propertyCount, const char **propertyKeys, const char **pro // Functions in HOST_RUNTIME_CONTRACT have priority over the individual properties // for callbacks, so we set them as long as the contract has a non-null function. struct host_runtime_contract* contract = (struct host_runtime_contract*)(uintptr_t)strtoull (propertyValues [i], NULL, 0); + mono_host_information_set_contract (contract); if (contract->pinvoke_override != NULL) { override_fn = (PInvokeOverrideFn)contract->pinvoke_override; } @@ -219,6 +301,9 @@ parse_properties (int propertyCount, const char **propertyKeys, const char **pro } } + if (!parse_trusted_platform_assemblies_from_contract () && tpa_property != NULL) + parse_trusted_platform_assemblies (tpa_property); + if (override_fn != NULL) mono_loader_install_pinvoke_override (override_fn); diff --git a/src/native/corehost/host_runtime_contract.h b/src/native/corehost/host_runtime_contract.h index 8bba040a6d5a03..fe03eeb4848a33 100644 --- a/src/native/corehost/host_runtime_contract.h +++ b/src/native/corehost/host_runtime_contract.h @@ -84,5 +84,20 @@ struct host_runtime_contract bool(HOST_CONTRACT_CALLTYPE* get_native_code_data)( const struct host_runtime_contract_native_code_context* context, /*out*/ struct host_runtime_contract_native_code_data* data); + + // Get the simple names of the host-resolved assemblies. + // Returned names are owned by the host and valid for the lifetime of the process. + bool(HOST_CONTRACT_CALLTYPE* get_assembly_names)( + /*out*/ const char* const** names, + /*out*/ size_t* count, + void* contract_context); + + // Resolve an assembly simple name to its path components. + // On success, returned strings are owned by the host and valid for the lifetime of the process. + bool(HOST_CONTRACT_CALLTYPE* resolve_assembly_to_path)( + const char* simple_name, + /*out*/ const char** directory, + /*out*/ const char** file_name, + void* contract_context); }; #endif // __HOST_RUNTIME_CONTRACT_H__ diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 6c18c17c2c14a6..2a5bfef3775ebb 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -410,6 +410,7 @@ namespace pal size_t pal_utf8string(const string_t& str, char* out_buffer, size_t len); bool pal_utf8string(const string_t& str, std::vector* out); + std::string pal_utf8string(const char_t* str, size_t length); bool pal_clrstring(const string_t& str, std::vector* out); bool clr_palstring(const char* cstr, string_t* out); @@ -479,6 +480,7 @@ namespace pal return len; } inline bool pal_utf8string(const string_t& str, std::vector* out) { out->assign(str.begin(), str.end()); out->push_back('\0'); return true; } + inline std::string pal_utf8string(const char_t* str, size_t length) { return std::string(str, length); } inline bool pal_clrstring(const string_t& str, std::vector* out) { return pal_utf8string(str, out); } inline bool clr_palstring(const char* cstr, string_t* out) { out->assign(cstr); return true; } diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index 9be59a1f6c1456..d3a47310753ba3 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -689,6 +689,23 @@ bool pal::pal_utf8string(const pal::string_t& str, std::vector* out) return ::WideCharToMultiByte(CP_UTF8, 0, str.c_str(), -1, out->data(), static_cast(out->size()), nullptr, nullptr) != 0; } +std::string pal::pal_utf8string(const pal::char_t* str, size_t length) +{ + if (length == 0) + return {}; + + int input_length = static_cast(length); + int size = ::WideCharToMultiByte(CP_UTF8, 0, str, input_length, nullptr, 0, nullptr, nullptr); + if (size == 0) + return {}; + + std::string out(static_cast(size), '\0'); + if (::WideCharToMultiByte(CP_UTF8, 0, str, input_length, out.data(), size, nullptr, nullptr) == 0) + return {}; + + return out; +} + bool pal::pal_clrstring(const pal::string_t& str, std::vector* out) { return pal_utf8string(str, out); diff --git a/src/native/corehost/hostpolicy/coreclr.cpp b/src/native/corehost/hostpolicy/coreclr.cpp index 8f71aa18c4a19a..b4fa4e3104ce20 100644 --- a/src/native/corehost/hostpolicy/coreclr.cpp +++ b/src/native/corehost/hostpolicy/coreclr.cpp @@ -155,7 +155,6 @@ namespace { const pal::char_t *PropertyNameMapping[] = { - _X("TRUSTED_PLATFORM_ASSEMBLIES"), _X("NATIVE_DLL_SEARCH_DIRECTORIES"), _X("PLATFORM_RESOURCE_ROOTS"), _X("APP_CONTEXT_BASE_DIRECTORY"), @@ -212,6 +211,11 @@ bool coreclr_property_bag_t::add(const pal::char_t *key, const pal::char_t *valu } } +bool coreclr_property_bag_t::contains(const pal::char_t *key) const +{ + return _properties.find(key) != _properties.cend(); +} + bool coreclr_property_bag_t::try_get(common_property key, const pal::char_t **value) const { int idx = static_cast(key); diff --git a/src/native/corehost/hostpolicy/coreclr.h b/src/native/corehost/hostpolicy/coreclr.h index 1ee29ba7edf98c..51d4d0187e4ddd 100644 --- a/src/native/corehost/hostpolicy/coreclr.h +++ b/src/native/corehost/hostpolicy/coreclr.h @@ -54,7 +54,6 @@ class coreclr_t enum class common_property { - TrustedPlatformAssemblies, NativeDllSearchDirectories, PlatformResourceRoots, AppContextBaseDirectory, @@ -81,6 +80,7 @@ class coreclr_property_bag_t bool add(common_property key, const pal::char_t *value); bool add(const pal::char_t *key, const pal::char_t *value); + bool contains(const pal::char_t *key) const; bool try_get(common_property key, const pal::char_t **value) const; bool try_get(const pal::char_t *key, const pal::char_t **value) const; diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index f6fd9b620835da..88247610cd7902 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -404,11 +404,37 @@ bool report_missing_assembly_in_manifest(const deps_entry_t& entry, bool continu return continueResolving; } +void probe_paths_t::tpa_t::add(pal::string_t&& path) +{ + size_t file_name_offset = path.find_last_of(DIR_SEPARATOR); + assert(file_name_offset != pal::string_t::npos); + ++file_name_offset; + assert(file_name_offset < path.size()); + + size_t directory_index = 0; + for (; directory_index < directories.size(); ++directory_index) + { + const entry_t& existing = entries[directories[directory_index]]; + if (existing.file_name_offset == file_name_offset && + path.compare(0, file_name_offset, existing.path, 0, existing.file_name_offset) == 0) + { + break; + } + } + + if (directory_index == directories.size()) + { + directories.push_back(entries.size()); + } + + entries.push_back({ std::move(path), directory_index, file_name_offset }); +} + /** * Resolve the TPA assembly locations */ bool deps_resolver_t::resolve_tpa_list( - pal::string_t* output, + probe_paths_t::tpa_t* output, std::unordered_set* breadcrumb, bool ignore_missing_assemblies) { @@ -565,11 +591,10 @@ bool deps_resolver_t::resolve_tpa_list( } } - // Convert the paths into a string and return it - for (const auto& item : items) + output->entries.reserve(output->entries.size() + items.size()); + for (std::pair& item : items) { - output->append(item.second.resolved_path); - output->push_back(PATH_SEPARATOR); + output->add(std::move(item.second.resolved_path)); } return true; diff --git a/src/native/corehost/hostpolicy/deps_resolver.h b/src/native/corehost/hostpolicy/deps_resolver.h index f7557de8f5d03b..239be0d1e5b486 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.h +++ b/src/native/corehost/hostpolicy/deps_resolver.h @@ -19,7 +19,23 @@ // Probe paths to be resolved for ordering struct probe_paths_t { - pal::string_t tpa; + struct tpa_t + { + struct entry_t + { + pal::string_t path; + size_t directory_index; + size_t file_name_offset; + }; + + // Indexes of entries whose path represents each unique directory. + std::vector directories; + std::vector entries; + + void add(pal::string_t&& path); + }; + + tpa_t tpa; pal::string_t native; pal::string_t resources; pal::string_t coreclr; @@ -230,7 +246,7 @@ class deps_resolver_t private: // Resolve order for TPA lookup. bool resolve_tpa_list( - pal::string_t* output, + probe_paths_t::tpa_t* output, std::unordered_set* breadcrumb, bool ignore_missing_assemblies); diff --git a/src/native/corehost/hostpolicy/hostpolicy.cpp b/src/native/corehost/hostpolicy/hostpolicy.cpp index 7d1befbca168a2..b6aa9b3b41c596 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -64,7 +64,31 @@ namespace // Verbose logging if (trace::is_enabled()) + { g_context->coreclr_properties.log_properties(); + if (!g_context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))) + { + for (const char* name : g_context->tpa_names) + { + std::unordered_map::const_iterator path = + g_context->tpa_paths.find(name); + assert(path != g_context->tpa_paths.end()); + + const char* directory = path->second.directory; + assert(directory[0] != '\0' && directory[strlen(directory) - 1] == static_cast(DIR_SEPARATOR)); + + pal::string_t name_str; + pal::string_t directory_str; + pal::string_t file_name_str; + if (pal::clr_palstring(name, &name_str) + && pal::clr_palstring(directory, &directory_str) + && pal::clr_palstring(path->second.file_name.c_str(), &file_name_str)) + { + trace::verbose(_X("TPA entry %s = %s%s"), name_str.c_str(), directory_str.c_str(), file_name_str.c_str()); + } + } + } + } std::vector host_path; pal::pal_clrstring(g_context->host_path, &host_path); @@ -558,7 +582,12 @@ namespace return StatusCode::HostInvalidState; if (!context->coreclr_properties.try_get(key, value)) - return StatusCode::HostPropertyNotFound; + { + if (pal::strcmp(key, _STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES)) != 0) + return StatusCode::HostPropertyNotFound; + + *value = context->get_reconstructed_tpa_property().c_str(); + } return StatusCode::Success; } @@ -599,7 +628,8 @@ namespace return StatusCode::HostInvalidState; } - size_t actualCount = context->coreclr_properties.count(); + bool hasExplicitTpa = context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES)); + size_t actualCount = context->coreclr_properties.count() + (hasExplicitTpa ? 0 : 1); size_t input_count = *count; *count = actualCount; if (input_count < actualCount || keys == nullptr || values == nullptr) @@ -613,6 +643,11 @@ namespace ++index; }; context->coreclr_properties.enumerate(callback); + if (!hasExplicitTpa) + { + keys[index] = _STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES); + values[index] = context->get_reconstructed_tpa_property().c_str(); + } return StatusCode::Success; } @@ -979,19 +1014,31 @@ SHARED_API int HOSTPOLICY_CALLTYPE corehost_resolve_component_dependencies( return StatusCode::ResolverResolveFailure; } + pal::string_t tpa; + for (const probe_paths_t::tpa_t::entry_t& entry : probe_paths.tpa.entries) + { + assert(entry.directory_index < probe_paths.tpa.directories.size()); + const probe_paths_t::tpa_t::entry_t& directory = + probe_paths.tpa.entries[probe_paths.tpa.directories[entry.directory_index]]; + assert(directory.file_name_offset != 0 && directory.path[directory.file_name_offset - 1] == DIR_SEPARATOR); + tpa.append(directory.path, 0, directory.file_name_offset); + tpa.append(entry.path, entry.file_name_offset, pal::string_t::npos); + tpa.push_back(PATH_SEPARATOR); + } + if (trace::is_enabled()) { trace::info(_X("corehost_resolve_component_dependencies results: {")); - trace::info(_X(" assembly_paths: '%s'"), probe_paths.tpa.data()); - trace::info(_X(" native_search_paths: '%s'"), probe_paths.native.data()); - trace::info(_X(" resource_search_paths: '%s'"), probe_paths.resources.data()); + trace::info(_X(" assembly_paths: '%s'"), tpa.c_str()); + trace::info(_X(" native_search_paths: '%s'"), probe_paths.native.c_str()); + trace::info(_X(" resource_search_paths: '%s'"), probe_paths.resources.c_str()); trace::info(_X("}")); } result( - probe_paths.tpa.data(), - probe_paths.native.data(), - probe_paths.resources.data()); + tpa.c_str(), + probe_paths.native.c_str(), + probe_paths.resources.c_str()); return 0; } diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index 010ea0de65a483..e89a57bf5c770f 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -20,6 +20,25 @@ namespace trace::error(_X("It is invalid to specify values for properties populated by the hosting layer in the application's .runtimeconfig.json")); } + std::string reconstruct_tpa_property(const hostpolicy_context_t* context) + { + std::string value; + for (const char* name : context->tpa_names) + { + std::unordered_map::const_iterator path = context->tpa_paths.find(name); + assert(path != context->tpa_paths.end()); + + if (!value.empty()) + value.push_back(static_cast(PATH_SEPARATOR)); + + assert(path->second.directory[0] != '\0' && path->second.directory[strlen(path->second.directory) - 1] == static_cast(DIR_SEPARATOR)); + value.append(path->second.directory); + value.append(path->second.file_name); + } + + return value; + } + // bundle_probe: // Probe the app-bundle for the file 'path' and return its location ('offset', 'size') if found. // @@ -120,6 +139,20 @@ namespace { hostpolicy_context_t* context = static_cast(contract_context); + if (::strcmp(key, HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES) == 0) + { + const pal::char_t* configuredValue; + if (context->coreclr_properties.try_get(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES), &configuredValue)) + return pal::pal_utf8string(configuredValue, value_buffer, value_buffer_size); + + std::string value = reconstruct_tpa_property(context); + size_t requiredSize = value.size() + 1; + if (value_buffer_size >= requiredSize) + memcpy(value_buffer, value.c_str(), requiredSize); + + return requiredSize; + } + // Properties computed on demand by the host if (::strcmp(key, HOST_PROPERTY_ENTRY_ASSEMBLY_NAME) == 0) { @@ -159,6 +192,50 @@ namespace return -1; } + + bool HOST_CONTRACT_CALLTYPE get_assembly_names( + const char* const** names, + size_t* count, + void* contract_context) + { + if (names == nullptr || count == nullptr) + return false; + + hostpolicy_context_t* context = static_cast(contract_context); + + // Custom host explicitly set the assemblies via a property string + if (context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))) + return false; + + *names = context->tpa_names.data(); + *count = context->tpa_names.size(); + return true; + } + + bool HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( + const char* simple_name, + const char** directory, + const char** file_name, + void* contract_context) + { + if (directory == nullptr || file_name == nullptr) + return false; + + hostpolicy_context_t* context = static_cast(contract_context); + + // Custom host explicitly set the assemblies via a property string + if (context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))) + return false; + + std::unordered_map::const_iterator entry = + context->tpa_paths.find(simple_name); + if (entry == context->tpa_paths.end()) + return false; + + *directory = entry->second.directory; + *file_name = entry->second.file_name.c_str(); + return true; + } } bool hostpolicy_context_t::should_read_rid_fallback_graph(const hostpolicy_init_t &init) @@ -174,6 +251,20 @@ bool hostpolicy_context_t::should_read_rid_fallback_graph(const hostpolicy_init_ return false; } +const pal::string_t& hostpolicy_context_t::get_reconstructed_tpa_property() +{ + assert(!coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))); + + std::call_once(reconstructed_tpa_property_once, [this]() + { + std::string value = reconstruct_tpa_property(this); + bool converted = pal::clr_palstring(value.c_str(), &reconstructed_tpa_property); + assert(converted); + }); + + return reconstructed_tpa_property; +} + int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs) { application = args.managed_application; @@ -243,12 +334,7 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c append_path(&corelib_path, CORELIB_NAME); // Append CoreLib path - if (!probe_paths.tpa.empty() && probe_paths.tpa.back() != PATH_SEPARATOR) - { - probe_paths.tpa.push_back(PATH_SEPARATOR); - } - - probe_paths.tpa.append(corelib_path); + probe_paths.tpa.add(std::move(corelib_path)); } pal::string_t fx_deps_str; @@ -282,7 +368,6 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c // Build properties for CoreCLR instantiation pal::string_t app_base; resolver.get_app_dir(&app_base); - coreclr_properties.add(common_property::TrustedPlatformAssemblies, probe_paths.tpa.c_str()); coreclr_properties.add(common_property::NativeDllSearchDirectories, probe_paths.native.c_str()); coreclr_properties.add(common_property::PlatformResourceRoots, probe_paths.resources.c_str()); coreclr_properties.add(common_property::AppContextBaseDirectory, app_base.c_str()); @@ -298,6 +383,12 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c { // Provide opt-in compatible behavior by using the switch to set APP_PATHS const pal::char_t *key = hostpolicy_init.cfg_keys[i].c_str(); + if (pal::strcmp(key, _STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES)) == 0) + { + log_duplicate_property_error(key); + return StatusCode::LibHostDuplicateProperty; + } + if (pal::strcasecmp(key, _X("Microsoft.NETCore.DotNetHostPolicy.SetAppPaths")) == 0) { set_app_paths = (pal::strcasecmp(hostpolicy_init.cfg_values[i].data(), _X("true")) == 0); @@ -322,6 +413,38 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c } } + tpa_directories.reserve(probe_paths.tpa.directories.size()); + for (size_t directory_entry_index : probe_paths.tpa.directories) + { + const probe_paths_t::tpa_t::entry_t& entry = probe_paths.tpa.entries[directory_entry_index]; + assert(entry.file_name_offset != 0 && entry.path[entry.file_name_offset - 1] == DIR_SEPARATOR); + tpa_directories.push_back( + pal::pal_utf8string(entry.path.data(), entry.file_name_offset)); + } + + tpa_names.reserve(probe_paths.tpa.entries.size()); + tpa_paths.reserve(probe_paths.tpa.entries.size()); + for (const probe_paths_t::tpa_t::entry_t& entry : probe_paths.tpa.entries) + { + assert(entry.directory_index < tpa_directories.size()); + tpa_path_t path{ + tpa_directories[entry.directory_index].c_str(), + pal::pal_utf8string( + entry.path.data() + entry.file_name_offset, + entry.path.size() - entry.file_name_offset) + }; + size_t extension_offset = entry.path.rfind(_X('.')); + size_t name_length = + extension_offset == pal::string_t::npos || extension_offset < entry.file_name_offset + ? entry.path.size() - entry.file_name_offset + : extension_offset - entry.file_name_offset; + std::string name = pal::pal_utf8string(entry.path.data() + entry.file_name_offset, name_length); + std::pair::iterator, bool> result = + tpa_paths.emplace(std::move(name), std::move(path)); + if (result.second) + tpa_names.push_back(result.first->first.c_str()); + } + // Startup hooks pal::string_t startup_hooks; if (pal::getenv(_X("DOTNET_STARTUP_HOOKS"), &startup_hooks)) @@ -349,6 +472,8 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c } host_contract.get_runtime_property = &get_runtime_property; + host_contract.get_assembly_names = &get_assembly_names; + host_contract.resolve_assembly_to_path = &resolve_assembly_to_path; pal::char_t ptr_to_string_buffer[STRING_LENGTH("0xffffffffffffffff") + 1]; pal::snwprintf(ptr_to_string_buffer, ARRAY_SIZE(ptr_to_string_buffer), _X("0x%zx"), (size_t)(&host_contract)); if (!coreclr_properties.add(_STRINGIFY(HOST_PROPERTY_RUNTIME_CONTRACT), ptr_to_string_buffer)) diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.h b/src/native/corehost/hostpolicy/hostpolicy_context.h index e815b59b038ae8..c845d44dee41a6 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.h +++ b/src/native/corehost/hostpolicy/hostpolicy_context.h @@ -15,6 +15,12 @@ struct hostpolicy_context_t { public: + struct tpa_path_t + { + const char* directory; + std::string file_name; + }; + pal::string_t application; pal::string_t clr_dir; pal::string_t clr_path; @@ -28,13 +34,21 @@ struct hostpolicy_context_t coreclr_property_bag_t coreclr_properties; std::unique_ptr coreclr; - host_runtime_contract host_contract; + std::vector tpa_names; + std::vector tpa_directories; + std::unordered_map tpa_paths; + int initialize(const hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs); + const pal::string_t& get_reconstructed_tpa_property(); public: // static static bool should_read_rid_fallback_graph(const hostpolicy_init_t &init); + +private: + std::once_flag reconstructed_tpa_property_once; + pal::string_t reconstructed_tpa_property; }; #endif // __HOSTPOLICY_CONTEXT_H__ diff --git a/src/native/corehost/test/mockcoreclr/mockcoreclr.cpp b/src/native/corehost/test/mockcoreclr/mockcoreclr.cpp index 3ea15bbf813495..c7e442a048c525 100644 --- a/src/native/corehost/test/mockcoreclr/mockcoreclr.cpp +++ b/src/native/corehost/test/mockcoreclr/mockcoreclr.cpp @@ -3,8 +3,10 @@ #include "mockcoreclr.h" #include +#include #include #include +#include #include "trace.h" #define MockLog(string)\ @@ -47,6 +49,19 @@ SHARED_API pal::hresult_t STDMETHODCALLTYPE coreclr_initialize( for (int i = 0; i < propertyCount; ++i) { MockLogEntry("property", propertyKeys[i], propertyValues[i]); + if (::strcmp(propertyKeys[i], HOST_PROPERTY_RUNTIME_CONTRACT) == 0) + { + host_runtime_contract* contract = reinterpret_cast(strtoull(propertyValues[i], nullptr, 0)); + size_t requiredSize = offsetof(host_runtime_contract, get_assembly_names) + sizeof(contract->get_assembly_names); + size_t assemblyNameCount = 0; + if (contract->size >= requiredSize && contract->get_assembly_names != nullptr) + { + const char* const* names; + if (!contract->get_assembly_names(&names, &assemblyNameCount, contract->context)) + assemblyNameCount = 0; + } + MockLogEntry("host_runtime_contract", "get_assembly_names", assemblyNameCount); + } } if (hostHandle != nullptr) diff --git a/src/native/public/mono/jit/details/mono-private-unstable-types.h b/src/native/public/mono/jit/details/mono-private-unstable-types.h index 64398893c56b70..8ce8bfe0de17db 100644 --- a/src/native/public/mono/jit/details/mono-private-unstable-types.h +++ b/src/native/public/mono/jit/details/mono-private-unstable-types.h @@ -35,9 +35,9 @@ typedef void (*MonovmRuntimeConfigArgumentsCleanup) (MonovmRuntimeConfi typedef struct { uint32_t assembly_count; - char **basenames; /* Foo.dll */ + char **basenames; /* Foo.dll for path-based entries; Foo for host-resolved entries */ uint32_t *basename_lens; - char **assembly_filepaths; /* /blah/blah/blah/Foo.dll */ + char **assembly_filepaths; /* /blah/blah/blah/Foo.dll; NULL for host-resolved entries */ } MonoCoreTrustedPlatformAssemblies; typedef struct { diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs new file mode 100644 index 00000000000000..3d4b3942e4c092 --- /dev/null +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Xunit; + +public class TrustedPlatformAssemblies +{ + public static bool HasSystemCoreLibFile => !string.IsNullOrEmpty(typeof(object).Assembly.Location); + + [ConditionalFact(typeof(TrustedPlatformAssemblies), nameof(HasSystemCoreLibFile))] + public static void IsAvailable() + { + string tpa = Assert.IsType(AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")); + string coreLibLocation = typeof(object).Assembly.Location; + + // On macOS, /tmp is a symlink to /private/tmp. Assembly.Location resolves the path, while host-provided TPA paths do not. + if (OperatingSystem.IsMacOS() && coreLibLocation.StartsWith("/private/tmp/", StringComparison.Ordinal)) + { + coreLibLocation = coreLibLocation["/private".Length..]; + } + + Assert.Contains(coreLibLocation, tpa.Split(Path.PathSeparator), StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesCallback.csproj b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesCallback.csproj new file mode 100644 index 00000000000000..b3b9f0151f1e86 --- /dev/null +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesCallback.csproj @@ -0,0 +1,8 @@ + + + true + + + + + diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj new file mode 100644 index 00000000000000..5bfd40c67dce20 --- /dev/null +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj @@ -0,0 +1,12 @@ + + + + true + true + + + + + + +