From ca07d53c59a4e65a1143e23b41ed95948ad62349 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 14 Aug 2026 15:35:00 -0700 Subject: [PATCH 01/14] Pass TPA assemblies through host contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- src/coreclr/binder/applicationcontext.cpp | 92 ++++++++++++------- src/coreclr/binder/assemblybindercommon.cpp | 31 ++++++- src/coreclr/binder/inc/applicationcontext.hpp | 7 -- src/coreclr/inc/hostinformation.h | 3 + src/coreclr/vm/appdomainnative.cpp | 20 +++- src/coreclr/vm/hostinformation.cpp | 37 +++++++- src/native/corehost/host_runtime_contract.h | 13 +++ src/native/corehost/hostmisc/pal.h | 2 + src/native/corehost/hostmisc/pal.windows.cpp | 16 ++++ src/native/corehost/hostpolicy/coreclr.cpp | 1 - src/native/corehost/hostpolicy/coreclr.h | 1 - .../corehost/hostpolicy/deps_resolver.cpp | 9 +- .../corehost/hostpolicy/deps_resolver.h | 4 +- src/native/corehost/hostpolicy/hostpolicy.cpp | 19 ++-- .../hostpolicy/hostpolicy_context.cpp | 75 +++++++++++++-- .../corehost/hostpolicy/hostpolicy_context.h | 3 +- 16 files changed, 265 insertions(+), 68 deletions(-) diff --git a/src/coreclr/binder/applicationcontext.cpp b/src/coreclr/binder/applicationcontext.cpp index 89b0bf66f7518f..b243f1873a5343 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,80 @@ 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; - } + SString 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; + mapEntry.m_wszSimpleName = wszSimpleName; + mapEntry.m_wszILFileName = 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 = nullptr; + if (pExistingEntry == nullptr) + { + wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; + if (wszSimpleName == nullptr) + { + GO_WITH_HRESULT(E_OUTOFMEMORY); + } + wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); + } + else + { + wszSimpleName = pExistingEntry->m_wszSimpleName; + } - 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()); + + SimpleNameToFileNameMapEntry mapEntry; + mapEntry.m_wszSimpleName = wszSimpleName; + mapEntry.m_wszILFileName = wszFileName; - m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); + m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); + } } // diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index b95cd254953aa8..6997d58ec26a1b 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,35 @@ 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 + { + SString tpaSimpleName(pTpaEntry->m_wszSimpleName); + HostInformation::ResolveAssemblyToPath(tpaSimpleName, fileName); + if (!fileName.IsEmpty()) + { + LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1]; + if (wszFileName == nullptr) + { + GO_WITH_HRESULT(E_OUTOFMEMORY); + } + wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode()); + + SimpleNameToFileNameMapEntry* mutableTpaEntry = + const_cast(pTpaEntry); + mutableTpaEntry->m_wszILFileName = wszFileName; + } + } + } + if (!fileName.IsEmpty()) + { ReleaseHolder pAssembly; SString getAssemblyDiag; hr = GetAssembly(fileName, @@ -1349,5 +1374,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/inc/hostinformation.h b/src/coreclr/inc/hostinformation.h index cf41ed5f4fdaf3..b219b66e848167 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(const SString& 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..7dcba082168906 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 @@ -157,6 +158,7 @@ extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall { if (pAppContext->IsTpaListProvided()) { + CRITSEC_Holder contextLock(pAppContext->GetCriticalSectionCookie()); BINDER_SPACE::SimpleNameToFileNameMap* pMap = pAppContext->GetTpaList(); _ASSERTE(pMap != NULL); @@ -165,12 +167,28 @@ 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 + { + SString simpleName(i->m_wszSimpleName); + HostInformation::ResolveAssemblyToPath(simpleName, 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..de257323057ed5 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,40 @@ 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(const SString& 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.GetUnicode()); + const char* resolvedPath = s_hostContract.resolve_assembly_to_path(utf8Name.GetUTF8(), s_hostContract.context); + if (resolvedPath == nullptr) + return; + + path.SetUTF8(resolvedPath); +} + bool HostInformation::HasExternalProbe() { size_t requiredSize = offsetof(host_runtime_contract, external_assembly_probe) + sizeof(s_hostContract.external_assembly_probe); diff --git a/src/native/corehost/host_runtime_contract.h b/src/native/corehost/host_runtime_contract.h index 8bba040a6d5a03..e4c8d87f6f7a3e 100644 --- a/src/native/corehost/host_runtime_contract.h +++ b/src/native/corehost/host_runtime_contract.h @@ -84,5 +84,18 @@ 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. + // Returned path is owned by the host and valid for the lifetime of the process. + const char* (HOST_CONTRACT_CALLTYPE* resolve_assembly_to_path)( + const char* simple_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..0ff99de52f3df5 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 string_t& str); 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 string_t& str) { return str; } 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..d91aa8a74875ff 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -689,6 +689,22 @@ 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::string_t& str) +{ + if (str.empty()) + return {}; + + int size = ::WideCharToMultiByte(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0, nullptr, nullptr); + if (size == 0) + return {}; + + std::string out(static_cast(size), '\0'); + if (::WideCharToMultiByte(CP_UTF8, 0, str.c_str(), static_cast(str.size()), 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..51184d5dfed2a5 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"), diff --git a/src/native/corehost/hostpolicy/coreclr.h b/src/native/corehost/hostpolicy/coreclr.h index 1ee29ba7edf98c..514a5d3fae37fb 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, diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index f6fd9b620835da..caed113d2f369f 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -408,7 +408,7 @@ bool report_missing_assembly_in_manifest(const deps_entry_t& entry, bool continu * Resolve the TPA assembly locations */ bool deps_resolver_t::resolve_tpa_list( - pal::string_t* output, + std::vector* output, std::unordered_set* breadcrumb, bool ignore_missing_assemblies) { @@ -565,11 +565,10 @@ bool deps_resolver_t::resolve_tpa_list( } } - // Convert the paths into a string and return it - for (const auto& item : items) + output->reserve(output->size() + items.size()); + for (auto& item : items) { - output->append(item.second.resolved_path); - output->push_back(PATH_SEPARATOR); + output->push_back(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..350a834b0a67ca 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.h +++ b/src/native/corehost/hostpolicy/deps_resolver.h @@ -19,7 +19,7 @@ // Probe paths to be resolved for ordering struct probe_paths_t { - pal::string_t tpa; + std::vector tpa; pal::string_t native; pal::string_t resources; pal::string_t coreclr; @@ -230,7 +230,7 @@ class deps_resolver_t private: // Resolve order for TPA lookup. bool resolve_tpa_list( - pal::string_t* output, + std::vector* 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..bd1d666cff35b7 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -979,19 +979,26 @@ SHARED_API int HOSTPOLICY_CALLTYPE corehost_resolve_component_dependencies( return StatusCode::ResolverResolveFailure; } + pal::string_t tpa; + for (const pal::string_t& entry : probe_paths.tpa) + { + tpa.append(entry); + 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..c3a86d1b4a78c7 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -120,6 +120,29 @@ namespace { hostpolicy_context_t* context = static_cast(contract_context); + if (::strcmp(key, HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES) == 0) + { + std::string value; + for (const char* name : context->trusted_platform_assembly_names) + { + std::unordered_map::const_iterator path = + context->trusted_platform_assembly_paths.find(name); + if (path == context->trusted_platform_assembly_paths.end()) + continue; + + if (!value.empty()) + value.push_back(static_cast(PATH_SEPARATOR)); + + value.append(path->second); + } + + 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 +182,30 @@ 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); + *names = context->trusted_platform_assembly_names.data(); + *count = context->trusted_platform_assembly_names.size(); + return true; + } + + const char* HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( + const char* simple_name, + void* contract_context) + { + hostpolicy_context_t* context = static_cast(contract_context); + std::unordered_map::const_iterator entry = + context->trusted_platform_assembly_paths.find(simple_name); + return entry == context->trusted_platform_assembly_paths.end() ? nullptr : entry->second.c_str(); + } } bool hostpolicy_context_t::should_read_rid_fallback_graph(const hostpolicy_init_t &init) @@ -243,12 +290,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.push_back(std::move(corelib_path)); } pal::string_t fx_deps_str; @@ -282,7 +324,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 +339,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, _X("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 +369,18 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c } } + trusted_platform_assembly_names.reserve(probe_paths.tpa.size()); + trusted_platform_assembly_paths.reserve(probe_paths.tpa.size()); + for (const pal::string_t& entry : probe_paths.tpa) + { + std::string name = pal::pal_utf8string(get_filename_without_ext(entry)); + std::string path = pal::pal_utf8string(entry); + std::pair::iterator, bool> result = + trusted_platform_assembly_paths.insert_or_assign(std::move(name), std::move(path)); + if (result.second) + trusted_platform_assembly_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 +408,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..dabc1f017a9caf 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.h +++ b/src/native/corehost/hostpolicy/hostpolicy_context.h @@ -28,8 +28,9 @@ struct hostpolicy_context_t coreclr_property_bag_t coreclr_properties; std::unique_ptr coreclr; - host_runtime_contract host_contract; + std::vector trusted_platform_assembly_names; + std::unordered_map trusted_platform_assembly_paths; int initialize(const hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs); From ed8a75ca6c4a1f5abde30f86cad41cc206c317be Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 17 Aug 2026 12:12:57 -0700 Subject: [PATCH 02/14] Update TPA contract tests and tracing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- .../HostApiInvokerApp/HostRuntimeContract.cs | 2 ++ ...ndencyResolutionCommandResultExtensions.cs | 34 +++++++++++++++++-- .../FrameworkDependentAppLaunch.cs | 20 ++++++++--- src/native/corehost/hostpolicy/hostpolicy.cpp | 13 +++++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs index 8894f3d7da114a..a81bd54f4a93ea 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/native/corehost/hostpolicy/hostpolicy.cpp b/src/native/corehost/hostpolicy/hostpolicy.cpp index bd1d666cff35b7..80f784c1e9832f 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -64,7 +64,20 @@ 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->trusted_platform_assembly_names) + { + std::unordered_map::const_iterator path = + g_context->trusted_platform_assembly_paths.find(name); + assert(path != g_context->trusted_platform_assembly_paths.end()); + + trace::verbose(_X("TPA entry %hs = %hs"), name, path->second.c_str()); + } + } + } std::vector host_path; pal::pal_clrstring(g_context->host_path, &host_path); From 92bc4f33ae472ce5f2dbc1f02865056364d7973a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 17 Aug 2026 12:13:32 -0700 Subject: [PATCH 03/14] Preserve explicit TPA host properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- .../NativeHosting/HostContext.cs | 44 +++++++++++++ .../HostContextResultExtensions.cs | 9 +++ src/native/corehost/hostpolicy/coreclr.cpp | 5 ++ src/native/corehost/hostpolicy/coreclr.h | 1 + src/native/corehost/hostpolicy/hostpolicy.cpp | 15 ++++- .../hostpolicy/hostpolicy_context.cpp | 61 ++++++++++++++----- .../corehost/hostpolicy/hostpolicy_context.h | 5 ++ .../corehost/test/mockcoreclr/mockcoreclr.cpp | 15 +++++ 8 files changed, 139 insertions(+), 16 deletions(-) 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/native/corehost/hostpolicy/coreclr.cpp b/src/native/corehost/hostpolicy/coreclr.cpp index 51184d5dfed2a5..b4fa4e3104ce20 100644 --- a/src/native/corehost/hostpolicy/coreclr.cpp +++ b/src/native/corehost/hostpolicy/coreclr.cpp @@ -211,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 514a5d3fae37fb..51d4d0187e4ddd 100644 --- a/src/native/corehost/hostpolicy/coreclr.h +++ b/src/native/corehost/hostpolicy/coreclr.h @@ -80,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/hostpolicy.cpp b/src/native/corehost/hostpolicy/hostpolicy.cpp index 80f784c1e9832f..5cf528a2ba574d 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -571,7 +571,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; } @@ -612,7 +617,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) @@ -626,6 +632,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; } diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index c3a86d1b4a78c7..704fdf6e37fcbd 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -20,6 +20,24 @@ 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->trusted_platform_assembly_names) + { + std::unordered_map::const_iterator path = + context->trusted_platform_assembly_paths.find(name); + assert(path != context->trusted_platform_assembly_paths.end()); + + if (!value.empty()) + value.push_back(static_cast(PATH_SEPARATOR)); + + value.append(path->second); + } + + return value; + } + // bundle_probe: // Probe the app-bundle for the file 'path' and return its location ('offset', 'size') if found. // @@ -122,20 +140,11 @@ namespace if (::strcmp(key, HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES) == 0) { - std::string value; - for (const char* name : context->trusted_platform_assembly_names) - { - std::unordered_map::const_iterator path = - context->trusted_platform_assembly_paths.find(name); - if (path == context->trusted_platform_assembly_paths.end()) - continue; - - if (!value.empty()) - value.push_back(static_cast(PATH_SEPARATOR)); - - value.append(path->second); - } + 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); @@ -192,6 +201,11 @@ namespace 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->trusted_platform_assembly_names.data(); *count = context->trusted_platform_assembly_names.size(); return true; @@ -202,6 +216,11 @@ namespace void* contract_context) { 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 nullptr; + std::unordered_map::const_iterator entry = context->trusted_platform_assembly_paths.find(simple_name); return entry == context->trusted_platform_assembly_paths.end() ? nullptr : entry->second.c_str(); @@ -221,6 +240,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; @@ -339,7 +372,7 @@ 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, _X("TRUSTED_PLATFORM_ASSEMBLIES")) == 0) + if (pal::strcmp(key, _STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES)) == 0) { log_duplicate_property_error(key); return StatusCode::LibHostDuplicateProperty; diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.h b/src/native/corehost/hostpolicy/hostpolicy_context.h index dabc1f017a9caf..10e3e68277ccc4 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.h +++ b/src/native/corehost/hostpolicy/hostpolicy_context.h @@ -33,9 +33,14 @@ struct hostpolicy_context_t std::unordered_map trusted_platform_assembly_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) From f3f09f736ffba39afae1a955aecb7f2878c6c06c Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 17 Aug 2026 15:52:05 -0700 Subject: [PATCH 04/14] Use TPA callbacks in CoreRun Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- src/coreclr/hosts/corerun/corerun.cpp | 114 ++++++++++++++---- src/coreclr/hosts/corerun/corerun.hpp | 1 - .../TrustedPlatformAssembliesProperty.cs | 15 +++ .../TrustedPlatformAssembliesProperty.csproj | 12 ++ 4 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs create mode 100644 src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj diff --git a/src/coreclr/hosts/corerun/corerun.cpp b/src/coreclr/hosts/corerun/corerun.cpp index 49908c2675ac74..f43f4dd05f6b3b 100644 --- a/src/coreclr/hosts/corerun/corerun.cpp +++ b/src/coreclr/hosts/corerun/corerun.cpp @@ -18,6 +18,7 @@ #endif // TARGET_BROWSER #include +#include #if defined(TARGET_UNIX) #include @@ -88,9 +89,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 +129,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 +140,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 +155,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 +163,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 +250,20 @@ static void log_error_info(const char* line) std::fprintf(stderr, "%s\n", line); } +struct host_runtime_contract_context +{ + const configuration* config; + std::vector assembly_names; + 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 +304,32 @@ 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 const char* HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( + const char* simple_name, + void* contract_context) +{ + host_runtime_contract_context* context = static_cast(contract_context); + std::unordered_map::const_iterator entry = context->assembly_paths.find(simple_name); + return entry == context->assembly_paths.end() ? nullptr : entry->second.c_str(); +} + // Paths for external assembly probe static char* s_core_libs_path = nullptr; static char* s_core_root_path = nullptr; @@ -452,25 +482,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 +565,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 +580,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 +607,32 @@ 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_paths.reserve(tpa.size()); + for (const std::pair& entry : tpa) + { + pal::string_utf8_t name = pal::convert_to_utf8(entry.first.c_str()); + pal::string_utf8_t path = pal::convert_to_utf8(entry.second.c_str()); + std::pair::iterator, bool> result = + contract_context.assembly_paths.insert_or_assign(name.c_str(), path.c_str()); + if (result.second) + contract_context.assembly_names.push_back(result.first->first.c_str()); + } + } + 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/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs new file mode 100644 index 00000000000000..11af614601935c --- /dev/null +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs @@ -0,0 +1,15 @@ +// 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 Xunit; + +public class TrustedPlatformAssembliesProperty +{ + [Fact] + public static void ExplicitPropertyIsUsed() + { + string tpa = Assert.IsType(AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")); + Assert.Contains(typeof(object).Assembly.Location, tpa, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj new file mode 100644 index 00000000000000..42340d0b24cb6f --- /dev/null +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj @@ -0,0 +1,12 @@ + + + + true + true + + + + + + + From b7529d8d6b887903a10e952df40704f3d0d8dd89 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 19 Aug 2026 16:27:32 -0700 Subject: [PATCH 05/14] Support TPA host callbacks in Mono Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- .../src/System/AppContext.CoreCLR.cs | 6 ++ .../src/System/AppContext.cs | 11 ++- src/mono/mono/metadata/icall-def.h | 3 + src/mono/mono/mini/CMakeLists.txt | 2 + src/mono/mono/mini/hostinformation.c | 41 ++++++++++ src/mono/mono/mini/hostinformation.h | 23 ++++++ src/mono/mono/mini/monovm.c | 81 ++++++++++++++++++- .../jit/details/mono-private-unstable-types.h | 2 +- 8 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 src/mono/mono/mini/hostinformation.c create mode 100644 src/mono/mono/mini/hostinformation.h 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/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..beb73f746d2091 --- /dev/null +++ b/src/mono/mono/mini/hostinformation.c @@ -0,0 +1,41 @@ +// 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); +} + +const char * +mono_host_information_resolve_assembly_to_path (const char *simple_name) +{ + if (!HOST_CONTRACT_HAS_FIELD (resolve_assembly_to_path) || host_contract.resolve_assembly_to_path == NULL) + return NULL; + + return host_contract.resolve_assembly_to_path (simple_name, host_contract.context); +} diff --git a/src/mono/mono/mini/hostinformation.h b/src/mono/mono/mini/hostinformation.h new file mode 100644 index 00000000000000..4635f93ca3a0fe --- /dev/null +++ b/src/mono/mono/mini/hostinformation.h @@ -0,0 +1,23 @@ +// 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); + +const char * +mono_host_information_resolve_assembly_to_path (const char *simple_name); + +#endif /* __MONO_MINI_HOSTINFORMATION_H__ */ diff --git a/src/mono/mono/mini/monovm.c b/src/mono/mono/mini/monovm.c index 9d9b1ae399b022..b1008ac1a9d0aa 100644 --- a/src/mono/mono/mini/monovm.c +++ b/src/mono/mono/mini/monovm.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,30 @@ 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->assembly_filepaths = g_new0 (char*, count + 1); + 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,24 @@ 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); 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. + gboolean has_fullpath = a->assembly_filepaths [i] != NULL; + const char *requested_name = has_fullpath ? basename : aname->name; + size_t requested_name_len = has_fullpath ? 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]; + const char *fullpath = has_fullpath + ? a->assembly_filepaths [i] + : mono_host_information_resolve_assembly_to_path (a->basenames [i]); + if (fullpath == NULL) + break; gboolean found = g_file_test (fullpath, G_FILE_TEST_IS_REGULAR); @@ -183,17 +217,52 @@ 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 *path = mono_host_information_resolve_assembly_to_path (trusted_platform_assemblies->basenames [i]); + if (path == NULL) + continue; + + if (property_value->len != 0) + g_string_append_c (property_value, G_SEARCHPATH_SEPARATOR); + g_string_append (property_value, 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 +277,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 +289,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/public/mono/jit/details/mono-private-unstable-types.h b/src/native/public/mono/jit/details/mono-private-unstable-types.h index 64398893c56b70..f3db12642d10c0 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,7 +35,7 @@ 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 */ } MonoCoreTrustedPlatformAssemblies; From c3ba6cb9d1290adeb41be258ffe2307a53f247ec Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 19 Aug 2026 16:27:51 -0700 Subject: [PATCH 06/14] Test TPA callback property reconstruction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- ...AssembliesProperty.cs => TrustedPlatformAssemblies.cs} | 4 ++-- .../TrustedPlatformAssembliesCallback.csproj | 8 ++++++++ .../TrustedPlatformAssembliesProperty.csproj | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) rename src/tests/Loader/TrustedPlatformAssembliesProperty/{TrustedPlatformAssembliesProperty.cs => TrustedPlatformAssemblies.cs} (80%) create mode 100644 src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesCallback.csproj diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs similarity index 80% rename from src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs rename to src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs index 11af614601935c..3bc89b3f7c3af5 100644 --- a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.cs +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs @@ -4,10 +4,10 @@ using System; using Xunit; -public class TrustedPlatformAssembliesProperty +public class TrustedPlatformAssemblies { [Fact] - public static void ExplicitPropertyIsUsed() + public static void IsAvailable() { string tpa = Assert.IsType(AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")); Assert.Contains(typeof(object).Assembly.Location, tpa, StringComparison.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 index 42340d0b24cb6f..5bfd40c67dce20 100644 --- a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssembliesProperty.csproj @@ -5,7 +5,7 @@ true - + From 30dc6dc34e9a2c790d4410dadb0e507c139ffce9 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 21 Aug 2026 11:22:51 -0700 Subject: [PATCH 07/14] Deduplicate TPA path directories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e234f49e-502f-4c6e-bacd-0971bb2033e7 --- src/coreclr/hosts/corerun/corerun.cpp | 48 ++++++++++++-- src/coreclr/vm/hostinformation.cpp | 15 ++++- .../HostApiInvokerApp/HostRuntimeContract.cs | 2 +- src/mono/mono/mini/hostinformation.c | 20 ++++-- src/mono/mono/mini/hostinformation.h | 7 +- src/mono/mono/mini/monovm.c | 50 ++++++++------ src/native/corehost/host_runtime_contract.h | 8 ++- .../corehost/hostpolicy/deps_resolver.cpp | 30 +++++++-- .../corehost/hostpolicy/deps_resolver.h | 19 +++++- src/native/corehost/hostpolicy/hostpolicy.cpp | 23 ++++--- .../hostpolicy/hostpolicy_context.cpp | 65 +++++++++++++------ .../corehost/hostpolicy/hostpolicy_context.h | 12 +++- 12 files changed, 223 insertions(+), 76 deletions(-) diff --git a/src/coreclr/hosts/corerun/corerun.cpp b/src/coreclr/hosts/corerun/corerun.cpp index f43f4dd05f6b3b..059dfd5f64e820 100644 --- a/src/coreclr/hosts/corerun/corerun.cpp +++ b/src/coreclr/hosts/corerun/corerun.cpp @@ -19,6 +19,7 @@ #include #include +#include #if defined(TARGET_UNIX) #include @@ -250,11 +251,18 @@ 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_map assembly_paths; + std::unordered_set assembly_directories; + std::unordered_map assembly_paths; }; size_t HOST_CONTRACT_CALLTYPE get_runtime_property( @@ -321,13 +329,24 @@ static bool HOST_CONTRACT_CALLTYPE get_assembly_names( return true; } -static const char* HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( +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); - return entry == context->assembly_paths.end() ? nullptr : entry->second.c_str(); + 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 @@ -611,16 +630,31 @@ static int run(const configuration& 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 path = pal::convert_to_utf8(entry.second.c_str()); - std::pair::iterator, bool> result = - contract_context.assembly_paths.insert_or_assign(name.c_str(), path.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 = { diff --git a/src/coreclr/vm/hostinformation.cpp b/src/coreclr/vm/hostinformation.cpp index de257323057ed5..e6dcabd5801b32 100644 --- a/src/coreclr/vm/hostinformation.cpp +++ b/src/coreclr/vm/hostinformation.cpp @@ -83,11 +83,20 @@ void HostInformation::ResolveAssemblyToPath(const SString& simpleName, SString& StackSString utf8Name; utf8Name.SetAndConvertToUTF8(simpleName.GetUnicode()); - const char* resolvedPath = s_hostContract.resolve_assembly_to_path(utf8Name.GetUTF8(), s_hostContract.context); - if (resolvedPath == nullptr) + const char* directory; + const char* fileName; + if (!s_hostContract.resolve_assembly_to_path(utf8Name.GetUTF8(), &directory, &fileName, s_hostContract.context)) return; - path.SetUTF8(resolvedPath); + 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() diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs index a81bd54f4a93ea..4694abfc2b6f30 100644 --- a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs +++ b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostRuntimeContract.cs @@ -21,7 +21,7 @@ internal struct host_runtime_contract 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; + public delegate* unmanaged[Stdcall] resolve_assembly_to_path; } #pragma warning restore CS0649 diff --git a/src/mono/mono/mini/hostinformation.c b/src/mono/mono/mini/hostinformation.c index beb73f746d2091..cf782cc3175ffc 100644 --- a/src/mono/mono/mini/hostinformation.c +++ b/src/mono/mono/mini/hostinformation.c @@ -31,11 +31,23 @@ mono_host_information_get_assembly_names (const char * const **names, size_t *co return host_contract.get_assembly_names (names, count, host_contract.context); } -const char * -mono_host_information_resolve_assembly_to_path (const char *simple_name) +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 NULL; + return FALSE; - return host_contract.resolve_assembly_to_path (simple_name, host_contract.context); + 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 index 4635f93ca3a0fe..20c57e3fe2ad24 100644 --- a/src/mono/mono/mini/hostinformation.h +++ b/src/mono/mono/mini/hostinformation.h @@ -17,7 +17,10 @@ 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); -const char * -mono_host_information_resolve_assembly_to_path (const char *simple_name); +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 b1008ac1a9d0aa..a11d1f5bf295a0 100644 --- a/src/mono/mono/mini/monovm.c +++ b/src/mono/mono/mini/monovm.c @@ -153,9 +153,14 @@ mono_core_preload_hook (MonoAssemblyLoadContext *alc, MonoAssemblyName *aname, c req.request.predicate = predicate; req.request.predicate_ud = predicate_ud; - const char *fullpath = has_fullpath - ? a->assembly_filepaths [i] - : mono_host_information_resolve_assembly_to_path (a->basenames [i]); + char *resolved_path = NULL; + const char *fullpath = a->assembly_filepaths [i]; + if (!has_fullpath) { + 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; @@ -165,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); @@ -183,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; } } @@ -231,13 +237,19 @@ ves_icall_System_AppContext_TryGetHostPropertyValue (MonoStringHandle name, Mono GString *property_value = g_string_new (NULL); for (guint32 i = 0; i < trusted_platform_assemblies->assembly_count; ++i) { - const char *path = mono_host_information_resolve_assembly_to_path (trusted_platform_assemblies->basenames [i]); - if (path == NULL) + 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) { diff --git a/src/native/corehost/host_runtime_contract.h b/src/native/corehost/host_runtime_contract.h index e4c8d87f6f7a3e..fe03eeb4848a33 100644 --- a/src/native/corehost/host_runtime_contract.h +++ b/src/native/corehost/host_runtime_contract.h @@ -92,10 +92,12 @@ struct host_runtime_contract /*out*/ size_t* count, void* contract_context); - // Resolve an assembly simple name to its path. - // Returned path is owned by the host and valid for the lifetime of the process. - const char* (HOST_CONTRACT_CALLTYPE* resolve_assembly_to_path)( + // 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/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index caed113d2f369f..3622129c6e5018 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -404,11 +404,33 @@ bool report_missing_assembly_in_manifest(const deps_entry_t& entry, bool continu return continueResolving; } +void probe_paths_t::tpa_t::add(const pal::string_t& path) +{ + pal::string_t directory = get_directory(path); + pal::string_t file_name = get_filename(path); + assert(!directory.empty() && directory.back() == DIR_SEPARATOR); + + std::vector::const_iterator existing = + std::find(directories.cbegin(), directories.cend(), directory); + size_t directory_index; + if (existing == directories.cend()) + { + directory_index = directories.size(); + directories.push_back(std::move(directory)); + } + else + { + directory_index = static_cast(existing - directories.cbegin()); + } + + entries.push_back({ directory_index, std::move(file_name) }); +} + /** * Resolve the TPA assembly locations */ bool deps_resolver_t::resolve_tpa_list( - std::vector* output, + probe_paths_t::tpa_t* output, std::unordered_set* breadcrumb, bool ignore_missing_assemblies) { @@ -565,10 +587,10 @@ bool deps_resolver_t::resolve_tpa_list( } } - output->reserve(output->size() + items.size()); - for (auto& item : items) + output->entries.reserve(output->entries.size() + items.size()); + for (const std::pair& item : items) { - output->push_back(std::move(item.second.resolved_path)); + output->add(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 350a834b0a67ca..7f900325c7a885 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.h +++ b/src/native/corehost/hostpolicy/deps_resolver.h @@ -19,7 +19,22 @@ // Probe paths to be resolved for ordering struct probe_paths_t { - std::vector tpa; + struct tpa_t + { + struct entry_t + { + size_t directory_index; + pal::string_t file_name; + }; + + // Directories are non-empty and end with DIR_SEPARATOR. + std::vector directories; + std::vector entries; + + void add(const pal::string_t& path); + }; + + tpa_t tpa; pal::string_t native; pal::string_t resources; pal::string_t coreclr; @@ -230,7 +245,7 @@ class deps_resolver_t private: // Resolve order for TPA lookup. bool resolve_tpa_list( - std::vector* 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 5cf528a2ba574d..5b550e1c169f39 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -68,13 +68,16 @@ namespace g_context->coreclr_properties.log_properties(); if (!g_context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))) { - for (const char* name : g_context->trusted_platform_assembly_names) + for (const char* name : g_context->tpa_names) { - std::unordered_map::const_iterator path = - g_context->trusted_platform_assembly_paths.find(name); - assert(path != g_context->trusted_platform_assembly_paths.end()); - - trace::verbose(_X("TPA entry %hs = %hs"), name, path->second.c_str()); + 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; + size_t directoryLength = strlen(directory); + assert(directoryLength != 0 && directory[directoryLength - 1] == static_cast(DIR_SEPARATOR)); + trace::verbose(_X("TPA entry %hs = %hs%hs"), name, directory, path->second.file_name.c_str()); } } } @@ -1004,9 +1007,13 @@ SHARED_API int HOSTPOLICY_CALLTYPE corehost_resolve_component_dependencies( } pal::string_t tpa; - for (const pal::string_t& entry : probe_paths.tpa) + for (const probe_paths_t::tpa_t::entry_t& entry : probe_paths.tpa.entries) { - tpa.append(entry); + assert(entry.directory_index < probe_paths.tpa.directories.size()); + const pal::string_t& directory = probe_paths.tpa.directories[entry.directory_index]; + assert(!directory.empty() && directory.back() == DIR_SEPARATOR); + tpa.append(directory); + tpa.append(entry.file_name); tpa.push_back(PATH_SEPARATOR); } diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index 704fdf6e37fcbd..781deccab27d8a 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -23,16 +23,18 @@ namespace std::string reconstruct_tpa_property(const hostpolicy_context_t* context) { std::string value; - for (const char* name : context->trusted_platform_assembly_names) + for (const char* name : context->tpa_names) { - std::unordered_map::const_iterator path = - context->trusted_platform_assembly_paths.find(name); - assert(path != context->trusted_platform_assembly_paths.end()); + 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)); - value.append(path->second); + size_t directory_length = strlen(path->second.directory); + assert(directory_length != 0 && path->second.directory[directory_length - 1] == static_cast(DIR_SEPARATOR)); + value.append(path->second.directory); + value.append(path->second.file_name); } return value; @@ -206,24 +208,34 @@ namespace if (context->coreclr_properties.contains(_STRINGIFY(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES))) return false; - *names = context->trusted_platform_assembly_names.data(); - *count = context->trusted_platform_assembly_names.size(); + *names = context->tpa_names.data(); + *count = context->tpa_names.size(); return true; } - const char* HOST_CONTRACT_CALLTYPE resolve_assembly_to_path( + 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 nullptr; + return false; + + std::unordered_map::const_iterator entry = + context->tpa_paths.find(simple_name); + if (entry == context->tpa_paths.end()) + return false; - std::unordered_map::const_iterator entry = - context->trusted_platform_assembly_paths.find(simple_name); - return entry == context->trusted_platform_assembly_paths.end() ? nullptr : entry->second.c_str(); + *directory = entry->second.directory; + *file_name = entry->second.file_name.c_str(); + return true; } } @@ -323,7 +335,7 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c append_path(&corelib_path, CORELIB_NAME); // Append CoreLib path - probe_paths.tpa.push_back(std::move(corelib_path)); + probe_paths.tpa.add(corelib_path); } pal::string_t fx_deps_str; @@ -402,16 +414,27 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c } } - trusted_platform_assembly_names.reserve(probe_paths.tpa.size()); - trusted_platform_assembly_paths.reserve(probe_paths.tpa.size()); - for (const pal::string_t& entry : probe_paths.tpa) + tpa_directories.reserve(probe_paths.tpa.directories.size()); + for (const pal::string_t& directory : probe_paths.tpa.directories) + { + assert(!directory.empty() && directory.back() == DIR_SEPARATOR); + tpa_directories.push_back(pal::pal_utf8string(directory)); + } + + 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) { - std::string name = pal::pal_utf8string(get_filename_without_ext(entry)); - std::string path = pal::pal_utf8string(entry); - std::pair::iterator, bool> result = - trusted_platform_assembly_paths.insert_or_assign(std::move(name), std::move(path)); + assert(entry.directory_index < tpa_directories.size()); + tpa_path_t path{ + tpa_directories[entry.directory_index].c_str(), + pal::pal_utf8string(entry.file_name) + }; + std::string name = pal::pal_utf8string(get_filename_without_ext(entry.file_name)); + std::pair::iterator, bool> result = + tpa_paths.emplace(std::move(name), std::move(path)); if (result.second) - trusted_platform_assembly_names.push_back(result.first->first.c_str()); + tpa_names.push_back(result.first->first.c_str()); } // Startup hooks diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.h b/src/native/corehost/hostpolicy/hostpolicy_context.h index 10e3e68277ccc4..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; @@ -29,8 +35,10 @@ struct hostpolicy_context_t std::unique_ptr coreclr; host_runtime_contract host_contract; - std::vector trusted_platform_assembly_names; - std::unordered_map trusted_platform_assembly_paths; + + 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(); From dec2d68f78918d47f3eefc9ee571b7eca6b57bea Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 24 Aug 2026 12:19:32 -0700 Subject: [PATCH 08/14] Use offsets into path entries when building TPA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 11aa1554-f420-4cb0-91cb-1bba7ec51725 --- src/coreclr/binder/applicationcontext.cpp | 27 ++++----------- src/native/corehost/hostmisc/pal.h | 4 +-- src/native/corehost/hostmisc/pal.windows.cpp | 9 ++--- .../corehost/hostpolicy/deps_resolver.cpp | 34 +++++++++++-------- .../corehost/hostpolicy/deps_resolver.h | 9 ++--- src/native/corehost/hostpolicy/hostpolicy.cpp | 9 ++--- .../hostpolicy/hostpolicy_context.cpp | 21 ++++++++---- 7 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/coreclr/binder/applicationcontext.cpp b/src/coreclr/binder/applicationcontext.cpp index b243f1873a5343..a3681a1a75e382 100644 --- a/src/coreclr/binder/applicationcontext.cpp +++ b/src/coreclr/binder/applicationcontext.cpp @@ -98,7 +98,7 @@ namespace BINDER_SPACE { for (size_t i = 0; i < assemblyCount; i++) { - SString simpleName(SString::Utf8, assemblyNames[i]); + StackSString simpleName(SString::Utf8, assemblyNames[i]); _ASSERT(!simpleName.IsEmpty()); if (m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode()) != nullptr) @@ -112,9 +112,7 @@ namespace BINDER_SPACE wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); - SimpleNameToFileNameMapEntry mapEntry; - mapEntry.m_wszSimpleName = wszSimpleName; - mapEntry.m_wszILFileName = nullptr; + SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, nullptr }; m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); } } @@ -138,20 +136,12 @@ namespace BINDER_SPACE continue; } - LPWSTR wszSimpleName = nullptr; - if (pExistingEntry == nullptr) - { - wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; - if (wszSimpleName == nullptr) - { - GO_WITH_HRESULT(E_OUTOFMEMORY); - } - wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); - } - else + LPWSTR wszSimpleName = new WCHAR[simpleName.GetCount() + 1]; + if (wszSimpleName == nullptr) { - wszSimpleName = pExistingEntry->m_wszSimpleName; + GO_WITH_HRESULT(E_OUTOFMEMORY); } + wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode()); LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1]; if (wszFileName == nullptr) @@ -160,10 +150,7 @@ namespace BINDER_SPACE } wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode()); - SimpleNameToFileNameMapEntry mapEntry; - mapEntry.m_wszSimpleName = wszSimpleName; - mapEntry.m_wszILFileName = wszFileName; - + SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, wszFileName }; m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry); } } diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 0ff99de52f3df5..2a5bfef3775ebb 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -410,7 +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 string_t& str); + 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); @@ -480,7 +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 string_t& str) { return str; } + 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 d91aa8a74875ff..d3a47310753ba3 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -689,17 +689,18 @@ 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::string_t& str) +std::string pal::pal_utf8string(const pal::char_t* str, size_t length) { - if (str.empty()) + if (length == 0) return {}; - int size = ::WideCharToMultiByte(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0, nullptr, nullptr); + 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.c_str(), static_cast(str.size()), out.data(), size, nullptr, nullptr) == 0) + if (::WideCharToMultiByte(CP_UTF8, 0, str, input_length, out.data(), size, nullptr, nullptr) == 0) return {}; return out; diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index 3622129c6e5018..88247610cd7902 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -404,26 +404,30 @@ bool report_missing_assembly_in_manifest(const deps_entry_t& entry, bool continu return continueResolving; } -void probe_paths_t::tpa_t::add(const pal::string_t& path) +void probe_paths_t::tpa_t::add(pal::string_t&& path) { - pal::string_t directory = get_directory(path); - pal::string_t file_name = get_filename(path); - assert(!directory.empty() && directory.back() == DIR_SEPARATOR); + 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()); - std::vector::const_iterator existing = - std::find(directories.cbegin(), directories.cend(), directory); - size_t directory_index; - if (existing == directories.cend()) + size_t directory_index = 0; + for (; directory_index < directories.size(); ++directory_index) { - directory_index = directories.size(); - directories.push_back(std::move(directory)); + 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; + } } - else + + if (directory_index == directories.size()) { - directory_index = static_cast(existing - directories.cbegin()); + directories.push_back(entries.size()); } - entries.push_back({ directory_index, std::move(file_name) }); + entries.push_back({ std::move(path), directory_index, file_name_offset }); } /** @@ -588,9 +592,9 @@ bool deps_resolver_t::resolve_tpa_list( } output->entries.reserve(output->entries.size() + items.size()); - for (const std::pair& item : items) + for (std::pair& item : items) { - output->add(item.second.resolved_path); + 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 7f900325c7a885..239be0d1e5b486 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.h +++ b/src/native/corehost/hostpolicy/deps_resolver.h @@ -23,15 +23,16 @@ struct probe_paths_t { struct entry_t { + pal::string_t path; size_t directory_index; - pal::string_t file_name; + size_t file_name_offset; }; - // Directories are non-empty and end with DIR_SEPARATOR. - std::vector directories; + // Indexes of entries whose path represents each unique directory. + std::vector directories; std::vector entries; - void add(const pal::string_t& path); + void add(pal::string_t&& path); }; tpa_t tpa; diff --git a/src/native/corehost/hostpolicy/hostpolicy.cpp b/src/native/corehost/hostpolicy/hostpolicy.cpp index 5b550e1c169f39..f180e7394adf33 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -1010,10 +1010,11 @@ SHARED_API int HOSTPOLICY_CALLTYPE corehost_resolve_component_dependencies( for (const probe_paths_t::tpa_t::entry_t& entry : probe_paths.tpa.entries) { assert(entry.directory_index < probe_paths.tpa.directories.size()); - const pal::string_t& directory = probe_paths.tpa.directories[entry.directory_index]; - assert(!directory.empty() && directory.back() == DIR_SEPARATOR); - tpa.append(directory); - tpa.append(entry.file_name); + 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); } diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index 781deccab27d8a..a7a31599143c3e 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -335,7 +335,7 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c append_path(&corelib_path, CORELIB_NAME); // Append CoreLib path - probe_paths.tpa.add(corelib_path); + probe_paths.tpa.add(std::move(corelib_path)); } pal::string_t fx_deps_str; @@ -415,10 +415,12 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c } tpa_directories.reserve(probe_paths.tpa.directories.size()); - for (const pal::string_t& directory : probe_paths.tpa.directories) + for (size_t directory_entry_index : probe_paths.tpa.directories) { - assert(!directory.empty() && directory.back() == DIR_SEPARATOR); - tpa_directories.push_back(pal::pal_utf8string(directory)); + 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()); @@ -428,9 +430,16 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c assert(entry.directory_index < tpa_directories.size()); tpa_path_t path{ tpa_directories[entry.directory_index].c_str(), - pal::pal_utf8string(entry.file_name) + pal::pal_utf8string( + entry.path.data() + entry.file_name_offset, + entry.path.size() - entry.file_name_offset) }; - std::string name = pal::pal_utf8string(get_filename_without_ext(entry.file_name)); + 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) From 3baf09406fb8d330f902e199abbd012356a7fc89 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 25 Aug 2026 13:13:21 -0700 Subject: [PATCH 09/14] Fix unix build --- src/native/corehost/hostpolicy/hostpolicy.cpp | 14 +++++++++++--- .../corehost/hostpolicy/hostpolicy_context.cpp | 3 +-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/native/corehost/hostpolicy/hostpolicy.cpp b/src/native/corehost/hostpolicy/hostpolicy.cpp index f180e7394adf33..b6aa9b3b41c596 100644 --- a/src/native/corehost/hostpolicy/hostpolicy.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy.cpp @@ -75,9 +75,17 @@ namespace assert(path != g_context->tpa_paths.end()); const char* directory = path->second.directory; - size_t directoryLength = strlen(directory); - assert(directoryLength != 0 && directory[directoryLength - 1] == static_cast(DIR_SEPARATOR)); - trace::verbose(_X("TPA entry %hs = %hs%hs"), name, directory, path->second.file_name.c_str()); + 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()); + } } } } diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index a7a31599143c3e..e89a57bf5c770f 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -31,8 +31,7 @@ namespace if (!value.empty()) value.push_back(static_cast(PATH_SEPARATOR)); - size_t directory_length = strlen(path->second.directory); - assert(directory_length != 0 && path->second.directory[directory_length - 1] == static_cast(DIR_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); } From 7e4fb8a4c247de2aef8a261b52b07bd5f7337f23 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 27 Aug 2026 13:33:22 -0700 Subject: [PATCH 10/14] Update doc --- .../features/host-runtime-information.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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). From 55426381f0cfa3845f85d0bb4385f8bd3e012d05 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 28 Aug 2026 11:48:38 -0700 Subject: [PATCH 11/14] Fix tests on macOS --- .../TrustedPlatformAssemblies.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs index 3bc89b3f7c3af5..f494c5ab801f18 100644 --- a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using Xunit; public class TrustedPlatformAssemblies @@ -10,6 +11,14 @@ public class TrustedPlatformAssemblies public static void IsAvailable() { string tpa = Assert.IsType(AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")); - Assert.Contains(typeof(object).Assembly.Location, tpa, StringComparison.OrdinalIgnoreCase); + 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); } } From 218296559f68fc9255660715b891d937a653e479 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 28 Aug 2026 13:20:03 -0700 Subject: [PATCH 12/14] Keep only names in runtime --- src/coreclr/binder/assemblybindercommon.cpp | 16 +--------------- src/coreclr/inc/hostinformation.h | 2 +- src/coreclr/vm/appdomainnative.cpp | 4 +--- src/coreclr/vm/hostinformation.cpp | 4 ++-- src/mono/mono/mini/monovm.c | 11 +++++------ .../jit/details/mono-private-unstable-types.h | 2 +- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index 6997d58ec26a1b..61a2232d5ca00d 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -907,21 +907,7 @@ namespace BINDER_SPACE } else { - SString tpaSimpleName(pTpaEntry->m_wszSimpleName); - HostInformation::ResolveAssemblyToPath(tpaSimpleName, fileName); - if (!fileName.IsEmpty()) - { - LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1]; - if (wszFileName == nullptr) - { - GO_WITH_HRESULT(E_OUTOFMEMORY); - } - wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode()); - - SimpleNameToFileNameMapEntry* mutableTpaEntry = - const_cast(pTpaEntry); - mutableTpaEntry->m_wszILFileName = wszFileName; - } + HostInformation::ResolveAssemblyToPath(pTpaEntry->m_wszSimpleName, fileName); } } diff --git a/src/coreclr/inc/hostinformation.h b/src/coreclr/inc/hostinformation.h index b219b66e848167..9784cefab661ef 100644 --- a/src/coreclr/inc/hostinformation.h +++ b/src/coreclr/inc/hostinformation.h @@ -13,7 +13,7 @@ class HostInformation 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(const SString& simpleName, SString& path); + 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 7dcba082168906..c51484c21916f6 100644 --- a/src/coreclr/vm/appdomainnative.cpp +++ b/src/coreclr/vm/appdomainnative.cpp @@ -158,7 +158,6 @@ extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall { if (pAppContext->IsTpaListProvided()) { - CRITSEC_Holder contextLock(pAppContext->GetCriticalSectionCookie()); BINDER_SPACE::SimpleNameToFileNameMap* pMap = pAppContext->GetTpaList(); _ASSERTE(pMap != NULL); @@ -174,8 +173,7 @@ extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall } else { - SString simpleName(i->m_wszSimpleName); - HostInformation::ResolveAssemblyToPath(simpleName, path); + HostInformation::ResolveAssemblyToPath(i->m_wszSimpleName, path); if (path.IsEmpty()) { ++i; diff --git a/src/coreclr/vm/hostinformation.cpp b/src/coreclr/vm/hostinformation.cpp index e6dcabd5801b32..65c82d3c3487a8 100644 --- a/src/coreclr/vm/hostinformation.cpp +++ b/src/coreclr/vm/hostinformation.cpp @@ -75,14 +75,14 @@ bool HostInformation::GetAssemblyNames(const char* const** names, size_t* count) return true; } -void HostInformation::ResolveAssemblyToPath(const SString& simpleName, SString& path) +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.GetUnicode()); + utf8Name.SetAndConvertToUTF8(simpleName); const char* directory; const char* fileName; if (!s_hostContract.resolve_assembly_to_path(utf8Name.GetUTF8(), &directory, &fileName, s_hostContract.context)) diff --git a/src/mono/mono/mini/monovm.c b/src/mono/mono/mini/monovm.c index a11d1f5bf295a0..c630df70ad55dd 100644 --- a/src/mono/mono/mini/monovm.c +++ b/src/mono/mono/mini/monovm.c @@ -86,7 +86,6 @@ parse_trusted_platform_assemblies_from_contract (void) MonoCoreTrustedPlatformAssemblies *a = g_new0 (MonoCoreTrustedPlatformAssemblies, 1); a->assembly_count = (uint32_t)count; - a->assembly_filepaths = g_new0 (char*, count + 1); a->basenames = g_new0 (char*, count + 1); a->basename_lens = g_new0 (uint32_t, count + 1); for (size_t i = 0; i < count; ++i) { @@ -141,12 +140,12 @@ 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) { // Host-resolved entries store simple names, while path-based entries store filenames with extensions. - gboolean has_fullpath = a->assembly_filepaths [i] != NULL; - const char *requested_name = has_fullpath ? basename : aname->name; - size_t requested_name_len = has_fullpath ? basename_len : simple_name_len; + 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); @@ -154,8 +153,8 @@ mono_core_preload_hook (MonoAssemblyLoadContext *alc, MonoAssemblyName *aname, c req.request.predicate_ud = predicate_ud; char *resolved_path = NULL; - const char *fullpath = a->assembly_filepaths [i]; - if (!has_fullpath) { + 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)) 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 f3db12642d10c0..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 @@ -37,7 +37,7 @@ typedef struct { uint32_t assembly_count; 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 { From 0d05f4379358899e57c57d60ea8366004d194a04 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 28 Aug 2026 15:54:58 -0700 Subject: [PATCH 13/14] Fix browser mono build --- src/mono/mono/mini/monovm.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/mono/mini/monovm.c b/src/mono/mono/mini/monovm.c index c630df70ad55dd..b2ee98ca5b1ee8 100644 --- a/src/mono/mono/mini/monovm.c +++ b/src/mono/mono/mini/monovm.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include From bce7e2bf53dbfb18ce8e9e9455bde97d99364e82 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 1 Sep 2026 15:35:09 -0700 Subject: [PATCH 14/14] Add ConditionalFact to test --- .../TrustedPlatformAssemblies.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs index f494c5ab801f18..3d4b3942e4c092 100644 --- a/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs +++ b/src/tests/Loader/TrustedPlatformAssembliesProperty/TrustedPlatformAssemblies.cs @@ -7,7 +7,9 @@ public class TrustedPlatformAssemblies { - [Fact] + 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"));