From 9cfcb7eee932e080e04efdc6c345df71b7e799ed Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 29 May 2026 11:47:49 -0700 Subject: [PATCH 01/14] Convert simple hostmisc pal helpers to C Add C implementations for pal_strdup, pal_fullpath, pal_file_exists, pal_readdir_onlydirectories (callback-based), and pal_is_running_in_wow64. These are the simple pal helpers fxr_resolver needs that can be converted before the install-location and utils helpers. Rewrite the corresponding pal::* C++ functions as thin delegating wrappers around the new C helpers, preserving the existing namespace and default parameter on pal::fullpath for C++ callers (deps_format.cpp etc). pal::realpath and the pattern overload of pal::readdir_onlydirectories remain unchanged (no current C consumer). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/hostmisc/pal.h | 27 ++++ src/native/corehost/hostmisc/pal.unix.c | 92 +++++++++++ src/native/corehost/hostmisc/pal.unix.cpp | 24 ++- src/native/corehost/hostmisc/pal.windows.c | 160 +++++++++++++++++++ src/native/corehost/hostmisc/pal.windows.cpp | 87 ++-------- 5 files changed, 314 insertions(+), 76 deletions(-) diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 014a97df6c45e2..6a958eb9703328 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -124,6 +124,13 @@ pal_char_t* pal_get_own_executable_path(void); bool pal_directory_exists(const pal_char_t* path); +// Returns true if the file or directory exists. Equivalent to pal::file_exists. +bool pal_file_exists(const pal_char_t* path); + +// Returns a heap-allocated, NUL-terminated copy of the given string, or +// NULL on allocation failure. Caller should free() the returned pointer. +pal_char_t* pal_strdup(const pal_char_t* str); + // Returns a heap-allocated, NUL-terminated copy of the named environment // variable's value, or NULL if the variable is unset or set to the empty // string. Caller must free() the returned pointer. @@ -142,6 +149,26 @@ static inline pal_char_t* pal_strndup(const pal_char_t* src, size_t len) return buf; } +// Canonicalize a path and verify it exists. Returns a heap-allocated, +// NUL-terminated canonical path, or NULL on failure. Caller should free() +// the returned pointer. When skip_error_logging is true, failure-path +// trace messages are suppressed (used when probing for optional files). +pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging); + +// Returns true if the current process is a 32-bit process running on a +// 64-bit Windows OS. Returns false on non-Windows. +bool pal_is_running_in_wow64(void); + +// Callback for pal_readdir_onlydirectories. Receives each directory entry +// name (just the leaf name, not a full path) and the caller-supplied context. +// Return true to continue enumeration, false to stop early. +typedef bool (*pal_readdir_callback_t)(const pal_char_t* entry_name, void* ctx); + +// Enumerate immediate subdirectories of path, invoking callback for each. +// Skips "." and "..". Returns true on full enumeration or callback-requested +// stop; returns false if the directory could not be opened. +bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx); + #ifdef __cplusplus } #endif diff --git a/src/native/corehost/hostmisc/pal.unix.c b/src/native/corehost/hostmisc/pal.unix.c index bcac4b9c617148..aff123d7fa73d9 100644 --- a/src/native/corehost/hostmisc/pal.unix.c +++ b/src/native/corehost/hostmisc/pal.unix.c @@ -4,12 +4,17 @@ // C implementations of the pal_* APIs needed by trace.c on non-Windows. #include "pal.h" +#include "trace.h" +#include +#include #include #include #include #include +#include +#include "config.h" #include pal_char_t* pal_get_own_executable_path(void) @@ -26,6 +31,11 @@ bool pal_directory_exists(const pal_char_t* path) return S_ISDIR(sb.st_mode); } +pal_char_t* pal_strdup(const pal_char_t* str) +{ + return strdup(str); +} + pal_char_t* pal_getenv(const pal_char_t* name) { const char* result = getenv(name); @@ -34,3 +44,85 @@ pal_char_t* pal_getenv(const pal_char_t* name) return strdup(result); } + +pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) +{ + if (path == NULL) + return NULL; + + char* resolved = realpath(path, NULL); + if (resolved == NULL) + { + if (errno != ENOENT && !skip_error_logging) + trace_error(_X("realpath(%s) failed: %s"), path, strerror(errno)); + return NULL; + } + + return resolved; +} + +bool pal_file_exists(const pal_char_t* path) +{ + return access(path, F_OK) == 0; +} + +bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx) +{ + if (path == NULL || callback == NULL) + return false; + + DIR* dir = opendir(path); + if (dir == NULL) + return false; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) + { +#if HAVE_DIRENT_D_TYPE + int entry_type = entry->d_type; +#else + int entry_type = DT_UNKNOWN; +#endif + + bool is_dir; + switch (entry_type) + { + case DT_DIR: + is_dir = true; + break; + + case DT_LNK: + case DT_UNKNOWN: + { + // Resolve via stat for symlinks and file systems that do not + // provide d_type. + struct stat sb; + if (fstatat(dirfd(dir), entry->d_name, &sb, 0) == -1) + continue; + + is_dir = S_ISDIR(sb.st_mode); + break; + } + + default: + continue; + } + + if (!is_dir) + continue; + + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + if (!callback(entry->d_name, ctx)) + break; + } + + closedir(dir); + return true; +} + +bool pal_is_running_in_wow64(void) +{ + return false; +} diff --git a/src/native/corehost/hostmisc/pal.unix.cpp b/src/native/corehost/hostmisc/pal.unix.cpp index f04c6f4746f705..b171551530612f 100644 --- a/src/native/corehost/hostmisc/pal.unix.cpp +++ b/src/native/corehost/hostmisc/pal.unix.cpp @@ -999,7 +999,16 @@ void pal::enumerate_environment_variables(const std::functionempty()) + return false; + + pal_char_t* resolved = ::pal_fullpath(path->c_str(), skip_error_logging); + if (resolved == nullptr) + return false; + + path->assign(resolved); + free(resolved); + return true; } bool pal::realpath(pal::string_t* path, bool skip_error_logging) @@ -1027,7 +1036,7 @@ bool pal::realpath(pal::string_t* path, bool skip_error_logging) bool pal::file_exists(const pal::string_t& path) { - return (::access(path.c_str(), F_OK) == 0); + return ::pal_file_exists(path.c_str()); } bool pal::is_directory(const pal::string_t& path) @@ -1130,12 +1139,19 @@ void pal::readdir_onlydirectories(const pal::string_t& path, const string_t& pat void pal::readdir_onlydirectories(const pal::string_t& path, std::vector* list) { - ::readdir(path, _X("*"), true, list); + assert(list != nullptr); + ::pal_readdir_onlydirectories(path.c_str(), + [](const pal_char_t* name, void* ctx) -> bool + { + static_cast*>(ctx)->emplace_back(name); + return true; + }, + list); } bool pal::is_running_in_wow64() { - return false; + return ::pal_is_running_in_wow64(); } bool pal::is_emulating_x64() diff --git a/src/native/corehost/hostmisc/pal.windows.c b/src/native/corehost/hostmisc/pal.windows.c index 7affbf2f2094d9..5177b78c1ef679 100644 --- a/src/native/corehost/hostmisc/pal.windows.c +++ b/src/native/corehost/hostmisc/pal.windows.c @@ -7,6 +7,7 @@ #include "trace.h" #include +#include pal_char_t* pal_get_own_executable_path(void) { @@ -46,6 +47,11 @@ bool pal_directory_exists(const pal_char_t* path) return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; } +pal_char_t* pal_strdup(const pal_char_t* str) +{ + return _wcsdup(str); +} + pal_char_t* pal_getenv(const pal_char_t* name) { DWORD needed = GetEnvironmentVariableW(name, NULL, 0); @@ -77,3 +83,157 @@ pal_char_t* pal_getenv(const pal_char_t* name) return result; } + +// Returns true if the path is already prefixed with one of the long-path +// extended-syntax prefixes: +// \\?\ (extended path prefix - includes \\?\UNC\) +// \\.\ (device path prefix) +// Mirrors LongFile::IsNormalized. +static bool is_path_normalized(const pal_char_t* path) +{ + if (path[0] == L'\0') + return true; + + return path[0] == L'\\' + && path[1] == L'\\' + && (path[2] == L'?' || path[2] == L'.') + && path[3] == L'\\'; +} + +pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) +{ + if (path == NULL || path[0] == L'\0') + return NULL; + + // Already-normalized (long-path-prefixed) paths are passed through as-is; + // GetFullPathNameW does not handle the \\?\ prefix correctly. + if (is_path_normalized(path)) + { + WIN32_FILE_ATTRIBUTE_DATA data; + if (GetFileAttributesExW(path, GetFileExInfoStandard, &data) != 0) + return pal_strdup(path); + // Fall through and let GetFullPathNameW have a chance, matching C++ pal::fullpath. + } + + // Start with a MAX_PATH-sized buffer; the typical case fits. + pal_char_t* buf = (pal_char_t*)malloc(MAX_PATH * sizeof(pal_char_t)); + if (buf == NULL) + return NULL; + + DWORD size = GetFullPathNameW(path, MAX_PATH, buf, NULL); + if (size == 0) + { + if (!skip_error_logging) + trace_error(_X("Error resolving full path [%s]"), path); + free(buf); + return NULL; + } + + if (size >= MAX_PATH) + { + // Need a larger buffer. Allocate enough room for the canonicalized + // path plus the longest long-path prefix ("\\?\UNC\" = 8 chars). + const DWORD prefix_headroom = 8; + pal_char_t* new_buf = (pal_char_t*)realloc(buf, (size + prefix_headroom) * sizeof(pal_char_t)); + if (new_buf == NULL) + { + free(buf); + return NULL; + } + buf = new_buf; + + DWORD new_size = GetFullPathNameW(path, size, buf, NULL); + if (new_size == 0 || new_size >= size) + { + if (!skip_error_logging) + trace_error(_X("Error resolving full path [%s]"), path); + free(buf); + return NULL; + } + + // Long paths require the \\?\ (or \\?\UNC\) prefix to be usable. + // For UNC paths (\\server\share\...), strip the leading "\\" and + // prepend "\\?\UNC\"; otherwise just prepend "\\?\". + bool is_unc = (buf[0] == L'\\' && buf[1] == L'\\'); + const pal_char_t* prefix = is_unc ? L"\\\\?\\UNC\\" : L"\\\\?\\"; + DWORD prefix_len = is_unc ? 8 : 4; + DWORD skip = is_unc ? 2 : 0; + + // Make room for the prefix by shifting the path right (including the NUL). + memmove(buf + prefix_len, buf + skip, (new_size - skip + 1) * sizeof(pal_char_t)); + memcpy(buf, prefix, prefix_len * sizeof(pal_char_t)); + } + + WIN32_FILE_ATTRIBUTE_DATA data; + if (GetFileAttributesExW(buf, GetFileExInfoStandard, &data) == 0) + { + free(buf); + return NULL; + } + + return buf; +} + +bool pal_file_exists(const pal_char_t* path) +{ + // Matches the C++ pal::file_exists semantics: canonicalize and verify + // existence (with logging suppressed). This handles long paths via the + // \\?\ prefix machinery in pal_fullpath. + pal_char_t* resolved = pal_fullpath(path, true); + bool exists = resolved != NULL; + free(resolved); + return exists; +} + +bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx) +{ + if (path == NULL || callback == NULL) + return false; + + // Build the search string: path + "\\*". One extra char beyond path + // length is needed for the separator if path doesn't already end with one. + size_t path_len = wcslen(path); + size_t search_len = path_len + 3; // worst case: '\\', '*', NUL + pal_char_t* search = (pal_char_t*)malloc(search_len * sizeof(pal_char_t)); + if (search == NULL) + return false; + + memcpy(search, path, path_len * sizeof(pal_char_t)); + size_t pos = path_len; + if (pos == 0 || (search[pos - 1] != L'\\' && search[pos - 1] != L'/')) + { + search[pos++] = L'\\'; + } + search[pos++] = L'*'; + search[pos] = L'\0'; + + WIN32_FIND_DATAW data = { 0 }; + HANDLE handle = FindFirstFileExW(search, FindExInfoStandard, &data, FindExSearchNameMatch, NULL, 0); + free(search); + if (handle == INVALID_HANDLE_VALUE) + return false; + + do + { + if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + continue; + + const pal_char_t* name = data.cFileName; + if (name[0] == L'.' && (name[1] == L'\0' || (name[1] == L'.' && name[2] == L'\0'))) + continue; + + if (!callback(name, ctx)) + break; + } while (FindNextFileW(handle, &data)); + + FindClose(handle); + return true; +} + +bool pal_is_running_in_wow64(void) +{ + BOOL is_wow64 = FALSE; + if (!IsWow64Process(GetCurrentProcess(), &is_wow64)) + return false; + return is_wow64 != FALSE; +} diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index 4dcec51446d9cc..aeeec8858f8ae3 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -962,79 +962,20 @@ bool pal::realpath(pal::string_t* path, bool skip_error_logging) bool pal::fullpath(string_t* path, bool skip_error_logging) { if (path->empty()) - { return false; - } - if (LongFile::IsNormalized(*path)) - { - WIN32_FILE_ATTRIBUTE_DATA data; - if (GetFileAttributesExW(path->c_str(), GetFileExInfoStandard, &data) != 0) - { - return true; - } - } - - char_t buf[MAX_PATH]; - size_t size = ::GetFullPathNameW(path->c_str(), MAX_PATH, buf, nullptr); - if (size == 0) - { - if (!skip_error_logging) - { - trace::error(_X("Error resolving full path [%s]"), path->c_str()); - } + pal_char_t* resolved = ::pal_fullpath(path->c_str(), skip_error_logging); + if (resolved == nullptr) return false; - } - string_t str; - if (size < MAX_PATH) - { - str.assign(buf); - } - else - { - str.resize(size + LongFile::UNCExtendedPathPrefix.length(), 0); - - size = ::GetFullPathNameW(path->c_str(), static_cast(size), (LPWSTR)str.data(), nullptr); - assert(size <= str.size()); - - if (size == 0) - { - if (!skip_error_logging) - { - trace::error(_X("Error resolving full path [%s]"), path->c_str()); - } - return false; - } - - const string_t* prefix = &LongFile::ExtendedPrefix; - //Check if the resolved path is a UNC. By default we assume relative path to resolve to disk - if (str.compare(0, LongFile::UNCPathPrefix.length(), LongFile::UNCPathPrefix) == 0) - { - prefix = &LongFile::UNCExtendedPathPrefix; - str.erase(0, LongFile::UNCPathPrefix.length()); - size = size - LongFile::UNCPathPrefix.length(); - } - - str.insert(0, *prefix); - str.resize(size + prefix->length()); - str.shrink_to_fit(); - } - - WIN32_FILE_ATTRIBUTE_DATA data; - if (GetFileAttributesExW(str.c_str(), GetFileExInfoStandard, &data) != 0) - { - *path = str; - return true; - } - - return false; + path->assign(resolved); + free(resolved); + return true; } bool pal::file_exists(const string_t& path) { - string_t tmp(path); - return pal::fullpath(&tmp, true); + return ::pal_file_exists(path.c_str()); } bool pal::is_directory(const pal::string_t& path) @@ -1098,17 +1039,19 @@ void pal::readdir_onlydirectories(const pal::string_t& path, const string_t& pat void pal::readdir_onlydirectories(const pal::string_t& path, std::vector* list) { - ::readdir(path, _X("*"), true, list); + assert(list != nullptr); + ::pal_readdir_onlydirectories(path.c_str(), + [](const pal_char_t* name, void* ctx) -> bool + { + static_cast*>(ctx)->emplace_back(name); + return true; + }, + list); } bool pal::is_running_in_wow64() { - BOOL fWow64Process = FALSE; - if (!IsWow64Process(GetCurrentProcess(), &fWow64Process)) - { - return false; - } - return (fWow64Process != FALSE); + return ::pal_is_running_in_wow64(); } typedef BOOL (WINAPI* is_wow64_process2)( From e1b5086c3d603131de13c85da889a31712295500 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 29 May 2026 12:28:36 -0700 Subject: [PATCH 02/14] Convert install-location hostmisc pal helpers to C Add C-callable versions of the no-arch install-location pal helpers used by fxr_resolver, plus a small C shim for the test-only env override: - pal_get_dotnet_self_registered_dir - pal_get_default_installation_dir - pal_get_dotnet_self_registered_config_location - pal_is_emulating_x64 - utils_test_only_getenv (delegates to C++ test_only_getenv) The C++ no-arch pal::* overloads become thin wrappers around the new C helpers. The arch-explicit *_for_arch overloads (still used by install_info.cpp) are left untouched. The _STRINGIFY macro and configure.h include are moved up to the shared section of pal.h (before extern ""C"") so the new C files can use _STRINGIFY(CURRENT_ARCH_NAME) to embed the build-time architecture name without duplicating the definitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/hostmisc/pal.h | 31 +- src/native/corehost/hostmisc/pal.unix.c | 296 +++++++++++++++++++ src/native/corehost/hostmisc/pal.unix.cpp | 41 +-- src/native/corehost/hostmisc/pal.windows.c | 248 ++++++++++++++++ src/native/corehost/hostmisc/pal.windows.cpp | 61 +--- src/native/corehost/hostmisc/utils.cpp | 10 + src/native/corehost/hostmisc/utils.h | 5 + 7 files changed, 616 insertions(+), 76 deletions(-) diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 6a958eb9703328..fa67bf26d397c5 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -112,6 +112,11 @@ typedef char pal_char_t; #endif // _WIN32 +#include "configure.h" + +// Wide-stringify the value of a macro: _STRINGIFY(FOO) -> _X(""). +#define _STRINGIFY(s) _X(s) + #ifdef __cplusplus extern "C" { #endif @@ -169,6 +174,28 @@ typedef bool (*pal_readdir_callback_t)(const pal_char_t* entry_name, void* ctx); // stop; returns false if the directory could not be opened. bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx); +// Returns true if the current process is an x64 process running under +// emulation on a non-x64 host (Windows x64-on-arm64 via WOW64, or macOS +// x64-on-arm64 via Rosetta). Returns false otherwise. +bool pal_is_emulating_x64(void); + +// Returns the directory containing the globally-registered .NET install for +// the current architecture, or NULL if no such registration exists. Caller +// should free() the returned pointer. +// Honors the test-only env var _DOTNET_TEST_GLOBALLY_REGISTERED_PATH and +// (on Windows) _DOTNET_TEST_REGISTRY_PATH. +pal_char_t* pal_get_dotnet_self_registered_dir(void); + +// Returns the default install directory of .NET for the current architecture, +// or NULL on failure. Caller should free() the returned pointer. +// Honors the test-only env var _DOTNET_TEST_DEFAULT_INSTALL_PATH. +pal_char_t* pal_get_default_installation_dir(void); + +// Returns a display string identifying the location consulted to discover the +// globally-registered install dir for the current architecture (registry path +// on Windows, file path on Unix). Caller should free() the returned pointer. +pal_char_t* pal_get_dotnet_self_registered_config_location(void); + #ifdef __cplusplus } #endif @@ -211,8 +238,6 @@ bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t #endif -#include "configure.h" - // When running on a platform that is not supported in RID fallback graph (because it was unknown // at the time the SharedFX in question was built), we need to use a reasonable fallback RID to allow // consuming the native assets. @@ -232,8 +257,6 @@ bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t #define LIB_FILE_EXT ".so" #endif -#define _STRINGIFY(s) _X(s) - #define LIB_NAME(NAME) LIB_PREFIX NAME #define LIB_FILE_NAME(NAME) LIB_PREFIX NAME LIB_FILE_EXT #define LIB_FILE_NAME_X(NAME) _STRINGIFY(LIB_FILE_NAME(NAME)) diff --git a/src/native/corehost/hostmisc/pal.unix.c b/src/native/corehost/hostmisc/pal.unix.c index aff123d7fa73d9..e43d324ef7d670 100644 --- a/src/native/corehost/hostmisc/pal.unix.c +++ b/src/native/corehost/hostmisc/pal.unix.c @@ -5,6 +5,7 @@ #include "pal.h" #include "trace.h" +#include "utils.h" #include #include @@ -14,8 +15,13 @@ #include #include +#if defined(TARGET_OSX) || defined(TARGET_FREEBSD) +#include +#endif + #include "config.h" #include +#include pal_char_t* pal_get_own_executable_path(void) { @@ -126,3 +132,293 @@ bool pal_is_running_in_wow64(void) { return false; } + +bool pal_is_emulating_x64(void) +{ + int is_translated_process = 0; +#if defined(TARGET_OSX) + size_t size = sizeof(is_translated_process); + if (sysctlbyname("sysctl.proc_translated", &is_translated_process, &size, NULL, 0) == -1) + { + trace_info(_X("Could not determine whether the current process is running under Rosetta.")); + if (errno != ENOENT) + { + trace_info(_X("Call to sysctlbyname failed: %s"), strerror(errno)); + } + + return false; + } +#endif + + return is_translated_process == 1; +} + +// ASCII-only lowercase of `src` into a fresh allocation. Used to lower-case +// architecture name segments embedded into file paths (matches the C++ side's +// defensive to_lower() over arch names). +static pal_char_t* ascii_lower_dup(const pal_char_t* src) +{ + if (src == NULL) + return NULL; + + size_t len = strlen(src); + pal_char_t* dup = (pal_char_t*)malloc(len + 1); + if (dup == NULL) + return NULL; + + for (size_t i = 0; i < len; ++i) + { + pal_char_t c = src[i]; + dup[i] = (c >= 'A' && c <= 'Z') ? (pal_char_t)(c + ('a' - 'A')) : c; + } + dup[len] = '\0'; + return dup; +} + +// Allocates the concatenation of `dir + '/' + leaf`. Returns NULL on +// allocation failure. +static pal_char_t* join_path_alloc(const pal_char_t* dir, const pal_char_t* leaf) +{ + size_t dir_len = strlen(dir); + size_t leaf_len = strlen(leaf); + bool need_sep = dir_len > 0 && dir[dir_len - 1] != '/'; + size_t total = dir_len + (need_sep ? 1 : 0) + leaf_len + 1; + + pal_char_t* out = (pal_char_t*)malloc(total); + if (out == NULL) + return NULL; + + memcpy(out, dir, dir_len); + if (need_sep) + out[dir_len] = '/'; + memcpy(out + dir_len + (need_sep ? 1 : 0), leaf, leaf_len); + out[total - 1] = '\0'; + return out; +} + +// Reads up to the first newline from `file`, returning the line (with the +// trailing '\n' stripped) as a fresh allocation in *out_line. +// Returns true only if a non-empty line was read. Mirrors the C++ +// `get_line_from_file` semantics: blank lines are NOT skipped, and an empty +// line (or pure-EOF) returns false. +static bool get_line_from_file(FILE* file, pal_char_t** out_line) +{ + *out_line = NULL; + + char buffer[256]; + pal_char_t* line = NULL; + size_t line_len = 0; + + while (fgets(buffer, sizeof(buffer), file)) + { + size_t chunk_len = strlen(buffer); + pal_char_t* grown = (pal_char_t*)realloc(line, line_len + chunk_len + 1); + if (grown == NULL) + { + free(line); + return false; + } + line = grown; + memcpy(line + line_len, buffer, chunk_len); + line_len += chunk_len; + line[line_len] = '\0'; + + if (line_len > 0 && line[line_len - 1] == '\n') + { + line[--line_len] = '\0'; + break; + } + } + + if (line == NULL || line_len == 0) + { + free(line); + return false; + } + + *out_line = line; + return true; +} + +// Reads the first non-blank-stripped line of `file_path` (the recorded install +// location). On success returns true and sets *out_location to a fresh +// allocation. *out_file_found is set to indicate whether the file existed +// (false → caller may attempt a fallback file; true → no fallback). +static bool get_install_location_from_file(const pal_char_t* file_path, bool* out_file_found, pal_char_t** out_location) +{ + *out_file_found = true; + *out_location = NULL; + + FILE* file = pal_file_open(file_path, _X("r")); + if (file == NULL) + { + if (errno == ENOENT) + { + trace_verbose(_X("The install_location file ['%s'] does not exist - skipping."), file_path); + *out_file_found = false; + } + else + { + trace_error(_X("The install_location file ['%s'] failed to open: %s."), file_path, strerror(errno)); + } + + return false; + } + + bool got_line = get_line_from_file(file, out_location); + fclose(file); + + if (!got_line) + { + trace_warning(_X("Did not find any install location in '%s'."), file_path); + return false; + } + + return true; +} + +// Computes "/install_location_". `base` defaults to +// "/etc/dotnet" but is overridable by _DOTNET_TEST_INSTALL_LOCATION_PATH. +static pal_char_t* compose_install_location_file_path(void) +{ + pal_char_t* base = utils_test_only_getenv(_X("_DOTNET_TEST_INSTALL_LOCATION_PATH")); + if (base == NULL) + { + base = pal_strdup(_X("/etc/dotnet")); + if (base == NULL) + return NULL; + } + + pal_char_t* arch_lower = ascii_lower_dup(_STRINGIFY(CURRENT_ARCH_NAME)); + if (arch_lower == NULL) + { + free(base); + return NULL; + } + + size_t base_len = strlen(base); + bool need_sep = base_len > 0 && base[base_len - 1] != '/'; + const char prefix[] = "install_location_"; + size_t prefix_len = ARRAY_SIZE(prefix) - 1; + size_t arch_len = strlen(arch_lower); + size_t total = base_len + (need_sep ? 1 : 0) + prefix_len + arch_len + 1; + + pal_char_t* out = (pal_char_t*)malloc(total); + if (out == NULL) + { + free(base); + free(arch_lower); + return NULL; + } + + memcpy(out, base, base_len); + size_t pos = base_len; + if (need_sep) + out[pos++] = '/'; + memcpy(out + pos, prefix, prefix_len); + pos += prefix_len; + memcpy(out + pos, arch_lower, arch_len); + out[total - 1] = '\0'; + + free(base); + free(arch_lower); + return out; +} + +pal_char_t* pal_get_dotnet_self_registered_config_location(void) +{ + return compose_install_location_file_path(); +} + +pal_char_t* pal_get_dotnet_self_registered_dir(void) +{ + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH")); + if (override != NULL) + return override; + + pal_char_t* arch_path = compose_install_location_file_path(); + if (arch_path == NULL) + return NULL; + + trace_verbose(_X("Looking for architecture-specific install_location file in '%s'."), arch_path); + + pal_char_t* location = NULL; + bool file_found = false; + bool ok = get_install_location_from_file(arch_path, &file_found, &location); + if (!ok && !file_found) + { + // For current architecture, fall back to the non-arch-specific file + // in the same base directory. + char* sep = strrchr(arch_path, '/'); + pal_char_t* legacy = NULL; + if (sep != NULL) + { + size_t base_len = (size_t)(sep - arch_path); + pal_char_t* base = (pal_char_t*)malloc(base_len + 1); + if (base != NULL) + { + memcpy(base, arch_path, base_len); + base[base_len] = '\0'; + legacy = join_path_alloc(base, _X("install_location")); + free(base); + } + } + else + { + legacy = pal_strdup(_X("install_location")); + } + + free(arch_path); + arch_path = NULL; + + if (legacy == NULL) + return NULL; + + trace_verbose(_X("Looking for install_location file in '%s'."), legacy); + ok = get_install_location_from_file(legacy, &file_found, &location); + free(legacy); + if (!ok) + return NULL; + } + else + { + free(arch_path); + if (!ok) + return NULL; + } + + trace_verbose(_X("Found registered install location '%s'."), location); + return location; +} + +pal_char_t* pal_get_default_installation_dir(void) +{ + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_DEFAULT_INSTALL_PATH")); + if (override != NULL) + return override; + +#if defined(TARGET_OSX) + const pal_char_t* base = _X("/usr/local/share/dotnet"); + if (pal_is_emulating_x64()) + { + return join_path_alloc(base, _STRINGIFY(CURRENT_ARCH_NAME)); + } + + return pal_strdup(base); +#elif defined(TARGET_FREEBSD) + int mib[2]; + char buf[PATH_MAX]; + size_t len = PATH_MAX; + + mib[0] = CTL_USER; + mib[1] = USER_LOCALBASE; + if (sysctl(mib, 2, buf, &len, NULL, 0) == 0) + { + return join_path_alloc(buf, _X("share/dotnet")); + } + + return pal_strdup(_X("/usr/local/share/dotnet")); +#else + return pal_strdup(_X("/usr/share/dotnet")); +#endif +} diff --git a/src/native/corehost/hostmisc/pal.unix.cpp b/src/native/corehost/hostmisc/pal.unix.cpp index b171551530612f..1100106150fe79 100644 --- a/src/native/corehost/hostmisc/pal.unix.cpp +++ b/src/native/corehost/hostmisc/pal.unix.cpp @@ -495,16 +495,14 @@ bool get_install_location_from_file(const pal::string_t& file_path, bool& file_f bool pal::get_dotnet_self_registered_dir(pal::string_t* recv) { - // ***Used only for testing*** - pal::string_t environment_override; - if (test_only_getenv(_X("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH"), &environment_override)) - { - recv->assign(environment_override); - return true; - } - // *************************** + recv->clear(); + pal::char_t* dir = pal_get_dotnet_self_registered_dir(); + if (dir == nullptr) + return false; - return pal::get_dotnet_self_registered_dir_for_arch(get_current_arch(), recv); + recv->assign(dir); + free(dir); + return true; } bool pal::get_dotnet_self_registered_dir_for_arch(pal::architecture arch, pal::string_t* recv) @@ -562,7 +560,13 @@ namespace bool pal::get_default_installation_dir(pal::string_t* recv) { - return get_default_installation_dir_for_arch(get_current_arch(), recv); + pal::char_t* dir = pal_get_default_installation_dir(); + if (dir == nullptr) + return false; + + recv->assign(dir); + free(dir); + return true; } bool pal::get_default_installation_dir_for_arch(pal::architecture arch, pal::string_t* recv) @@ -1156,22 +1160,7 @@ bool pal::is_running_in_wow64() bool pal::is_emulating_x64() { - int is_translated_process = 0; -#if defined(TARGET_OSX) - size_t size = sizeof(is_translated_process); - if (sysctlbyname("sysctl.proc_translated", &is_translated_process, &size, NULL, 0) == -1) - { - trace::info(_X("Could not determine whether the current process is running under Rosetta.")); - if (errno != ENOENT) - { - trace::info(_X("Call to sysctlbyname failed: %s"), strerror(errno).c_str()); - } - - return false; - } -#endif - - return is_translated_process == 1; + return pal_is_emulating_x64(); } bool pal::are_paths_equal_with_normalized_casing(const string_t& path1, const string_t& path2) diff --git a/src/native/corehost/hostmisc/pal.windows.c b/src/native/corehost/hostmisc/pal.windows.c index 5177b78c1ef679..fbd5946ff4ceba 100644 --- a/src/native/corehost/hostmisc/pal.windows.c +++ b/src/native/corehost/hostmisc/pal.windows.c @@ -5,9 +5,13 @@ #include "pal.h" #include "trace.h" +#include "utils.h" #include #include +#include + +#include pal_char_t* pal_get_own_executable_path(void) { @@ -237,3 +241,247 @@ bool pal_is_running_in_wow64(void) return false; return is_wow64 != FALSE; } + +typedef BOOL(WINAPI* is_wow64_process2_fn)(HANDLE, USHORT*, USHORT*); + +bool pal_is_emulating_x64(void) +{ +#if defined(TARGET_AMD64) + HMODULE kernel32 = LoadLibraryExW(L"kernel32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (kernel32 == NULL) + { + trace_info(_X("Could not load 'kernel32.dll': %u"), GetLastError()); + return false; + } + + is_wow64_process2_fn is_wow64_process2_func = (is_wow64_process2_fn)(void*)GetProcAddress(kernel32, "IsWow64Process2"); + if (is_wow64_process2_func == NULL) + { + return false; + } + + USHORT process_machine; + USHORT native_machine; + if (!is_wow64_process2_func(GetCurrentProcess(), &process_machine, &native_machine)) + { + trace_info(_X("Call to IsWow64Process2 failed: %u"), GetLastError()); + return false; + } + + return native_machine != IMAGE_FILE_MACHINE_AMD64; +#else + return false; +#endif +} + +// Resolves the registry hive and sub-key path consulted to discover the +// globally-registered .NET install for the current architecture. The value +// name is always "InstallLocation" (REG_SZ). +// Honors _DOTNET_TEST_REGISTRY_PATH to replace the default SOFTWARE\dotnet +// base path; if the override begins with the literal "HKEY_CURRENT_USER\" +// (case-sensitive), the hive is switched to HKCU and the prefix is stripped. +// Returns true on success; on failure *out_sub_key is NULL. +static bool get_dotnet_install_location_registry_path(HKEY* out_hive, pal_char_t** out_sub_key) +{ + *out_hive = HKEY_LOCAL_MACHINE; + *out_sub_key = NULL; + + pal_char_t* base = NULL; + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_REGISTRY_PATH")); + if (override != NULL) + { + const pal_char_t hkcu_prefix[] = _X("HKEY_CURRENT_USER\\"); + const size_t hkcu_prefix_len = ARRAY_SIZE(hkcu_prefix) - 1; + if (wcsncmp(override, hkcu_prefix, hkcu_prefix_len) == 0) + { + *out_hive = HKEY_CURRENT_USER; + base = pal_strdup(override + hkcu_prefix_len); + } + else + { + base = override; + override = NULL; + } + + free(override); + if (base == NULL) + return false; + } + else + { + base = pal_strdup(_X("SOFTWARE\\dotnet")); + if (base == NULL) + return false; + } + + const pal_char_t suffix[] = _X("\\Setup\\InstalledVersions\\"); + const pal_char_t* arch = _STRINGIFY(CURRENT_ARCH_NAME); + size_t base_len = wcslen(base); + size_t suffix_len = ARRAY_SIZE(suffix) - 1; + size_t arch_len = wcslen(arch); + size_t total = base_len + suffix_len + arch_len + 1; + + pal_char_t* combined = (pal_char_t*)malloc(total * sizeof(pal_char_t)); + if (combined == NULL) + { + free(base); + return false; + } + + memcpy(combined, base, base_len * sizeof(pal_char_t)); + memcpy(combined + base_len, suffix, suffix_len * sizeof(pal_char_t)); + memcpy(combined + base_len + suffix_len, arch, arch_len * sizeof(pal_char_t)); + combined[total - 1] = L'\0'; + + free(base); + *out_sub_key = combined; + return true; +} + +// Allocates "HKLM\\InstallLocation" or "HKCU\..." for display/tracing. +static pal_char_t* format_registry_path(HKEY hive, const pal_char_t* sub_key) +{ + const pal_char_t* prefix = (hive == HKEY_CURRENT_USER) ? _X("HKCU\\") : _X("HKLM\\"); + const pal_char_t value[] = _X("\\InstallLocation"); + + size_t prefix_len = wcslen(prefix); + size_t sub_key_len = wcslen(sub_key); + size_t value_len = ARRAY_SIZE(value) - 1; + size_t total = prefix_len + sub_key_len + value_len + 1; + + pal_char_t* result = (pal_char_t*)malloc(total * sizeof(pal_char_t)); + if (result == NULL) + return NULL; + + memcpy(result, prefix, prefix_len * sizeof(pal_char_t)); + memcpy(result + prefix_len, sub_key, sub_key_len * sizeof(pal_char_t)); + memcpy(result + prefix_len + sub_key_len, value, value_len * sizeof(pal_char_t)); + result[total - 1] = L'\0'; + return result; +} + +pal_char_t* pal_get_dotnet_self_registered_config_location(void) +{ + HKEY hive; + pal_char_t* sub_key; + if (!get_dotnet_install_location_registry_path(&hive, &sub_key)) + return NULL; + + pal_char_t* result = format_registry_path(hive, sub_key); + free(sub_key); + return result; +} + +pal_char_t* pal_get_dotnet_self_registered_dir(void) +{ + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH")); + if (override != NULL) + return override; + + HKEY hive; + pal_char_t* sub_key; + if (!get_dotnet_install_location_registry_path(&hive, &sub_key)) + return NULL; + + if (trace_is_enabled()) + { + pal_char_t* display = format_registry_path(hive, sub_key); + if (display != NULL) + { + trace_verbose(_X("Looking for architecture-specific registry value in '%s'."), display); + free(display); + } + } + + // RegOpenKeyEx is required to force the 32-bit registry view via KEY_WOW64_32KEY. + HKEY hkey = NULL; + LSTATUS status = RegOpenKeyExW(hive, sub_key, 0, KEY_READ | KEY_WOW64_32KEY, &hkey); + if (status != ERROR_SUCCESS) + { + if (status == ERROR_FILE_NOT_FOUND) + trace_verbose(_X("The registry key ['%s'] does not exist."), sub_key); + else + trace_verbose(_X("Failed to open the registry key. Error code: 0x%X"), (unsigned int)status); + + free(sub_key); + return NULL; + } + + free(sub_key); + + const pal_char_t* value = _X("InstallLocation"); + + // RegGetValueW reports size in BYTES (including null terminator on REG_SZ). + DWORD size_bytes = 0; + status = RegGetValueW(hkey, NULL, value, RRF_RT_REG_SZ, NULL, NULL, &size_bytes); + if (status != ERROR_SUCCESS || size_bytes == 0) + { + trace_verbose(_X("Failed to get the size of the install location registry value or it's empty. Error code: 0x%X"), (unsigned int)status); + RegCloseKey(hkey); + return NULL; + } + + pal_char_t* buffer = (pal_char_t*)malloc(size_bytes); + if (buffer == NULL) + { + RegCloseKey(hkey); + return NULL; + } + + status = RegGetValueW(hkey, NULL, value, RRF_RT_REG_SZ, NULL, buffer, &size_bytes); + RegCloseKey(hkey); + if (status != ERROR_SUCCESS) + { + trace_verbose(_X("Failed to get the value of the install location registry value. Error code: 0x%X"), (unsigned int)status); + free(buffer); + return NULL; + } + + trace_verbose(_X("Found registered install location '%s'."), buffer); + return buffer; +} + +pal_char_t* pal_get_default_installation_dir(void) +{ + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_DEFAULT_INSTALL_PATH")); + if (override != NULL) + return override; + + pal_char_t* program_files = pal_getenv(_X("ProgramFiles")); + if (program_files == NULL) + return NULL; + + pal_char_t* canonical = pal_fullpath(program_files, /*skip_error_logging*/ false); + if (canonical == NULL) + { + trace_verbose(_X("Did not find [%s] directory [%s]"), _X("ProgramFiles"), program_files); + free(program_files); + return NULL; + } + + free(program_files); + + // Append "\dotnet" (and "\x64" if emulating x64). + const pal_char_t dotnet_seg[] = _X("\\dotnet"); + const pal_char_t arch_seg[] = _X("\\") _STRINGIFY(CURRENT_ARCH_NAME); + + size_t canonical_len = wcslen(canonical); + size_t dotnet_len = ARRAY_SIZE(dotnet_seg) - 1; + bool emulating_x64 = pal_is_emulating_x64(); + size_t arch_len = emulating_x64 ? (ARRAY_SIZE(arch_seg) - 1) : 0; + size_t total = canonical_len + dotnet_len + arch_len + 1; + + pal_char_t* result = (pal_char_t*)realloc(canonical, total * sizeof(pal_char_t)); + if (result == NULL) + { + free(canonical); + return NULL; + } + + memcpy(result + canonical_len, dotnet_seg, dotnet_len * sizeof(pal_char_t)); + if (emulating_x64) + memcpy(result + canonical_len + dotnet_len, arch_seg, arch_len * sizeof(pal_char_t)); + result[total - 1] = L'\0'; + + return result; +} diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index aeeec8858f8ae3..635294510d911b 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -391,7 +391,13 @@ namespace bool pal::get_default_installation_dir(pal::string_t* recv) { - return get_default_installation_dir_for_arch(get_current_arch(), recv); + pal::char_t* dir = pal_get_default_installation_dir(); + if (dir == nullptr) + return false; + + recv->assign(dir); + free(dir); + return true; } bool pal::get_default_installation_dir_for_arch(pal::architecture arch, pal::string_t* recv) @@ -504,16 +510,14 @@ pal::string_t pal::get_dotnet_self_registered_config_location(pal::architecture bool pal::get_dotnet_self_registered_dir(pal::string_t* recv) { - // ***Used only for testing*** - pal::string_t environmentOverride; - if (test_only_getenv(_X("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH"), &environmentOverride)) - { - recv->assign(environmentOverride); - return true; - } - // *************************** + recv->clear(); + pal::char_t* dir = pal_get_dotnet_self_registered_dir(); + if (dir == nullptr) + return false; - return get_dotnet_self_registered_dir_for_arch(get_current_arch(), recv); + recv->assign(dir); + free(dir); + return true; } bool pal::get_dotnet_self_registered_dir_for_arch(pal::architecture arch, pal::string_t* recv) @@ -1054,44 +1058,9 @@ bool pal::is_running_in_wow64() return ::pal_is_running_in_wow64(); } -typedef BOOL (WINAPI* is_wow64_process2)( - HANDLE hProcess, - USHORT *pProcessMachine, - USHORT *pNativeMachine -); - bool pal::is_emulating_x64() { -#if defined(TARGET_AMD64) - auto kernel32 = LoadLibraryExW(L"kernel32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - if (kernel32 == nullptr) - { - // Loading kernel32.dll failed, log the error and continue. - trace::info(_X("Could not load 'kernel32.dll': %u"), GetLastError()); - return false; - } - - is_wow64_process2 is_wow64_process2_func = (is_wow64_process2)::GetProcAddress(kernel32, "IsWow64Process2"); - if (is_wow64_process2_func == nullptr) - { - // Could not find IsWow64Process2. - return false; - } - - USHORT process_machine; - USHORT native_machine; - if (!is_wow64_process2_func(GetCurrentProcess(), &process_machine, &native_machine)) - { - // IsWow64Process2 failed. Log the error and continue. - trace::info(_X("Call to IsWow64Process2 failed: %u"), GetLastError()); - return false; - } - - // If we are running targeting x64 on a non-x64 machine, we are emulating - return native_machine != IMAGE_FILE_MACHINE_AMD64; -#else - return false; -#endif + return pal_is_emulating_x64(); } bool pal::are_paths_equal_with_normalized_casing(const string_t& path1, const string_t& path2) diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index 560c0273807d0c..af9c6be6fc6b05 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -538,3 +538,13 @@ bool test_only_getenv(const pal::char_t* name, pal::string_t* recv) return pal::getenv(name, recv); } + +extern "C" pal_char_t* utils_test_only_getenv(const pal_char_t* name) +{ + pal::string_t value; + if (!test_only_getenv(name, &value)) + return nullptr; + + pal_char_t* dup = pal_strdup(value.c_str()); + return dup; +} diff --git a/src/native/corehost/hostmisc/utils.h b/src/native/corehost/hostmisc/utils.h index 1f601214565c39..50308963464f36 100644 --- a/src/native/corehost/hostmisc/utils.h +++ b/src/native/corehost/hostmisc/utils.h @@ -218,6 +218,11 @@ extern "C" { // caller-supplied buffer (out_name is set to empty). bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len); +// Retrieves the value of a test-only environment variable. Returns NULL when the +// variable is unset or when the product binary is not stamped as a test build. +// Caller must free() the returned pointer. +pal_char_t* utils_test_only_getenv(const pal_char_t* name); + #ifdef __cplusplus } #endif From bdfe94e21a79fc03d8c302216aaf8a372aebf5af Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 29 May 2026 13:04:50 -0700 Subject: [PATCH 03/14] Convert utils helpers used by fxr_resolver to C - utils_append_path - utils_file_exists_in_dir_alloc - utils_get_directory_alloc - utils_get_dotnet_root_env_var_for_current_arch - utils_get_file_path_from_env - utils_get_dotnet_root_from_env - utils_get_download_url (C-callable shim around the existing C++ impl) C++ counterparts in utils.cpp become thin wrappers around the new C helpers where the data shape allows. append_path and get_dotnet_root_env_var_for_arch keep their existing C++ implementations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/CMakeLists.txt | 2 + src/native/corehost/configure.h.in | 1 + src/native/corehost/hostmisc/utils.c | 153 ++++++++++++++++++++++++- src/native/corehost/hostmisc/utils.cpp | 92 ++++++--------- src/native/corehost/hostmisc/utils.h | 41 ++++++- 5 files changed, 222 insertions(+), 67 deletions(-) diff --git a/src/native/corehost/CMakeLists.txt b/src/native/corehost/CMakeLists.txt index c586dffc1bbcf4..07996e246de1ca 100644 --- a/src/native/corehost/CMakeLists.txt +++ b/src/native/corehost/CMakeLists.txt @@ -43,6 +43,8 @@ if("${CLI_CMAKE_FALLBACK_OS}" STREQUAL "${CLR_CMAKE_TARGET_OS}") add_compile_definitions(FALLBACK_OS_IS_SAME_AS_TARGET_OS) endif() +string(TOUPPER "${CLR_CMAKE_TARGET_ARCH}" CLR_CMAKE_TARGET_ARCH_UPPER) + # Find support libraries that we need if they're present on the system. find_library(PTHREAD_LIB pthread) diff --git a/src/native/corehost/configure.h.in b/src/native/corehost/configure.h.in index 7d766945a9ccae..dcd894a1297715 100644 --- a/src/native/corehost/configure.h.in +++ b/src/native/corehost/configure.h.in @@ -15,5 +15,6 @@ #define FALLBACK_HOST_OS "@CLI_CMAKE_FALLBACK_OS@" #define CURRENT_OS_NAME "@CLR_CMAKE_TARGET_OS@" #define CURRENT_ARCH_NAME "@CLR_CMAKE_TARGET_ARCH@" +#define CURRENT_ARCH_NAME_UPPER "@CLR_CMAKE_TARGET_ARCH_UPPER@" #endif // PAL_HOST_CONFIGURE_H_INCLUDED diff --git a/src/native/corehost/hostmisc/utils.c b/src/native/corehost/hostmisc/utils.c index 3cb5329f4eb367..7c017fcf476a94 100644 --- a/src/native/corehost/hostmisc/utils.c +++ b/src/native/corehost/hostmisc/utils.c @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// C implementations of the utils_* APIs needed by trace.c. +// C implementations of the utils_* APIs needed by trace.c and fxr_resolver.c. #include "utils.h" @@ -31,3 +31,154 @@ bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out memcpy(out_name, name, (len + 1) * sizeof(pal_char_t)); return true; } + +void utils_append_path(pal_char_t* path, size_t path_len, const pal_char_t* component) +{ + if (component == NULL || component[0] == _X('\0')) + return; + + size_t current_len = pal_strlen(path); + size_t comp_len = pal_strlen(component); + + if (current_len == 0) + { + if (comp_len < path_len) + memcpy(path, component, (comp_len + 1) * sizeof(pal_char_t)); + return; + } + + bool need_sep = (path[current_len - 1] != DIR_SEPARATOR) && (component[0] != DIR_SEPARATOR); + if (current_len + (need_sep ? 1u : 0u) + comp_len >= path_len) + return; + + if (need_sep) + path[current_len++] = DIR_SEPARATOR; + + memcpy(path + current_len, component, (comp_len + 1) * sizeof(pal_char_t)); +} + +pal_char_t* utils_file_exists_in_dir_alloc(const pal_char_t* dir, const pal_char_t* file_name) +{ + if (dir == NULL || file_name == NULL) + return NULL; + + size_t cap = pal_strlen(dir) + pal_strlen(file_name) + 2; // +1 separator, +1 NUL + pal_char_t* file_path = (pal_char_t*)malloc(cap * sizeof(pal_char_t)); + file_path[0] = _X('\0'); + utils_append_path(file_path, cap, dir); + utils_append_path(file_path, cap, file_name); + + if (!pal_file_exists(file_path)) + { + free(file_path); + return NULL; + } + + return file_path; +} + +pal_char_t* utils_get_directory_alloc(const pal_char_t* path) +{ + if (path == NULL) + return NULL; + + // Drop trailing separators (matches C++ get_directory which pops them off the working copy). + size_t len = pal_strlen(path); + while (len > 0 && path[len - 1] == DIR_SEPARATOR) + len--; + + // Find the last separator within [0, len). + intptr_t sep_pos = -1; + for (intptr_t i = (intptr_t)len - 1; i >= 0; i--) + { + if (path[i] == DIR_SEPARATOR) + { + sep_pos = i; + break; + } + } + + size_t prefix_len; + if (sep_pos < 0) + { + // No separator: result is path[0..len) + DIR_SEPARATOR. + prefix_len = len; + } + else + { + // Skip any run of separators that precedes sep_pos (matches the C++ pos-- loop). + intptr_t pos = sep_pos; + while (pos >= 0 && path[pos] == DIR_SEPARATOR) + pos--; + prefix_len = (size_t)(pos + 1); + } + + pal_char_t* result = (pal_char_t*)malloc((prefix_len + 2) * sizeof(pal_char_t)); + if (result == NULL) + return NULL; + + if (prefix_len > 0) + memcpy(result, path, prefix_len * sizeof(pal_char_t)); + result[prefix_len] = DIR_SEPARATOR; + result[prefix_len + 1] = _X('\0'); + return result; +} + +pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key) +{ + pal_char_t* file_path = pal_getenv(env_key); + if (file_path == NULL) + return NULL; + + pal_char_t* canonical = pal_fullpath(file_path, /* skip_error_logging */ false); + if (canonical == NULL) + { + trace_verbose(_X("Did not find [%s] directory [%s]"), env_key, file_path); + free(file_path); + return NULL; + } + + free(file_path); + return canonical; +} + +bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root) +{ + if (out_env_var_name == NULL || out_dotnet_root == NULL) + return false; + + *out_env_var_name = NULL; + *out_dotnet_root = NULL; + + const pal_char_t* arch_env_var_name = DOTNET_ROOT_ARCH_ENV_VAR; + pal_char_t* dotnet_root = utils_get_file_path_from_env(arch_env_var_name); + if (dotnet_root != NULL) + { + *out_env_var_name = arch_env_var_name; + *out_dotnet_root = dotnet_root; + return true; + } + +#if defined(_WIN32) + if (pal_is_running_in_wow64()) + { + dotnet_root = utils_get_file_path_from_env(_X("DOTNET_ROOT(x86)")); + if (dotnet_root != NULL) + { + *out_env_var_name = _X("DOTNET_ROOT(x86)"); + *out_dotnet_root = dotnet_root; + return true; + } + } +#endif + + dotnet_root = utils_get_file_path_from_env(DOTNET_ROOT_ENV_VAR); + if (dotnet_root != NULL) + { + *out_env_var_name = DOTNET_ROOT_ENV_VAR; + *out_dotnet_root = dotnet_root; + return true; + } + + return false; +} diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index af9c6be6fc6b05..a7ed5626117226 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -12,15 +12,13 @@ bool file_exists_in_dir(const pal::string_t& dir, const pal::char_t* file_name, pal::string_t* out_file_path) { - pal::string_t file_path = dir; - append_path(&file_path, file_name); - - if (!pal::file_exists(file_path)) + pal_char_t* file_path = utils_file_exists_in_dir_alloc(dir.c_str(), file_name); + if (file_path == nullptr) return false; - if (out_file_path) - *out_file_path = file_path; - + if (out_file_path != nullptr) + out_file_path->assign(file_path); + free(file_path); return true; } @@ -137,25 +135,13 @@ pal::string_t get_filename(const pal::string_t& path) pal::string_t get_directory(const pal::string_t& path) { - pal::string_t ret = path; - while (!ret.empty() && ret.back() == DIR_SEPARATOR) - { - ret.pop_back(); - } + pal_char_t* result = utils_get_directory_alloc(path.c_str()); + if (result == nullptr) + return pal::string_t(); - // Find the last dir separator - auto path_sep = ret.find_last_of(DIR_SEPARATOR); - if (path_sep == pal::string_t::npos) - { - return ret + DIR_SEPARATOR; - } - - int pos = static_cast(path_sep); - while (pos >= 0 && ret[pos] == DIR_SEPARATOR) - { - pos--; - } - return ret.substr(0, static_cast(pos) + 1) + DIR_SEPARATOR; + pal::string_t ret(result); + free(result); + return ret; } void remove_trailing_dir_separator(pal::string_t* dir) @@ -323,18 +309,13 @@ void get_framework_locations(const pal::string_t& dotnet_dir, const bool disable bool get_file_path_from_env(const pal::char_t* env_key, pal::string_t* recv) { recv->clear(); - pal::string_t file_path; - if (pal::getenv(env_key, &file_path)) - { - if (pal::fullpath(&file_path)) - { - recv->assign(file_path); - return true; - } - trace::verbose(_X("Did not find [%s] directory [%s]"), env_key, file_path.c_str()); - } + pal_char_t* canonical = utils_get_file_path_from_env(env_key); + if (canonical == nullptr) + return false; - return false; + recv->assign(canonical); + free(canonical); + return true; } size_t index_of_non_numeric(const pal::string_t& str, size_t i) @@ -363,33 +344,18 @@ pal::string_t get_dotnet_root_env_var_for_arch(pal::architecture arch) bool get_dotnet_root_from_env(pal::string_t* dotnet_root_env_var_name, pal::string_t* recv) { - pal::string_t env_var_name = get_dotnet_root_env_var_for_arch(get_current_arch()); - if (get_file_path_from_env(env_var_name.c_str(), recv)) - { - *dotnet_root_env_var_name = env_var_name; - return true; - } - -#if defined(WIN32) - if (pal::is_running_in_wow64()) + const pal_char_t* env_var_name = nullptr; + pal_char_t* dotnet_root = nullptr; + if (!utils_get_dotnet_root_from_env(&env_var_name, &dotnet_root)) { - if (get_file_path_from_env(_X("DOTNET_ROOT(x86)"), recv)) - { - *dotnet_root_env_var_name = _X("DOTNET_ROOT(x86)"); - return true; - } - } -#endif - - // If no architecture-specific environment variable was set - // fallback to the default DOTNET_ROOT. - if (get_file_path_from_env(DOTNET_ROOT_ENV_VAR, recv)) - { - *dotnet_root_env_var_name = DOTNET_ROOT_ENV_VAR; - return true; + recv->clear(); + return false; } - return false; + dotnet_root_env_var_name->assign(env_var_name); + recv->assign(dotnet_root); + free(dotnet_root); + return true; } /** @@ -548,3 +514,9 @@ extern "C" pal_char_t* utils_test_only_getenv(const pal_char_t* name) pal_char_t* dup = pal_strdup(value.c_str()); return dup; } + +extern "C" pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version) +{ + pal::string_t url = get_download_url(framework_name, framework_version); + return pal_strdup(url.c_str()); +} diff --git a/src/native/corehost/hostmisc/utils.h b/src/native/corehost/hostmisc/utils.h index 50308963464f36..c6718d7fc00803 100644 --- a/src/native/corehost/hostmisc/utils.h +++ b/src/native/corehost/hostmisc/utils.h @@ -42,6 +42,7 @@ _X("%s&apphost_version=%s") #define DOTNET_ROOT_ENV_VAR _X("DOTNET_ROOT") +#define DOTNET_ROOT_ARCH_ENV_VAR DOTNET_ROOT_ENV_VAR _X("_") _STRINGIFY(CURRENT_ARCH_NAME_UPPER) #define SDK_DOTNET_DLL _X("dotnet.dll") @@ -213,16 +214,44 @@ size_t to_size_t_dbgchecked(T value) extern "C" { #endif -// Writes the trailing path component (i.e. text after the last DIR_SEPARATOR) -// of `path` into `out_name`. Returns false if the result does not fit in the -// caller-supplied buffer (out_name is set to empty). +// Write the trailing path component of `path` (the text after the last +// DIR_SEPARATOR) into out_name. Return false if it does not fit. bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len); -// Retrieves the value of a test-only environment variable. Returns NULL when the -// variable is unset or when the product binary is not stamped as a test build. -// Caller must free() the returned pointer. +// Return the value of a test-only environment variable, or NULL when unset +// or when the product binary is not stamped as a test build. Caller should +// free() the returned pointer. pal_char_t* utils_test_only_getenv(const pal_char_t* name); +// Append `component` to the caller-allocated buffer `path`, inserting a +// DIR_SEPARATOR between them when needed. Silently no-op on overflow or +// when `component` is NULL/empty. +void utils_append_path(pal_char_t* path, size_t path_len, const pal_char_t* component); + +// Return / if the file exists, otherwise NULL. Caller should +// free() the returned pointer. +pal_char_t* utils_file_exists_in_dir_alloc(const pal_char_t* dir, const pal_char_t* file_name); + +// Return the directory portion of `path`, always ending with DIR_SEPARATOR. +// Caller should free() the returned pointer. +pal_char_t* utils_get_directory_alloc(const pal_char_t* path); + +// Read `env_key` and return the canonicalized value, or NULL if unset or +// canonicalization fails. Caller should free() the returned pointer. +pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key); + +// Find the .NET install root from environment variables in priority order: +// - DOTNET_ROOT_ +// - If running Windows WOW64 only, DOTNET_ROOT(x86) +// - DOTNET_ROOT +// Caller should free() out_dotnet_root. +bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root); + +// Return the download URL for `framework_name` at `framework_version`, or +// the runtime URL when `framework_name` is NULL or empty. Caller should free() +// the returned pointer. +pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version); + #ifdef __cplusplus } #endif From 9016e32b44ba49ddfc5ef6395f80f76f3b01654b Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 2 Jun 2026 14:53:12 -0700 Subject: [PATCH 04/14] Convert fxr_resolver to C - get_latest_fxr (static) - fxr_resolver_try_get_path - fxr_resolver_try_get_path_from_dotnet_root try_get_existing_fxr stays in C++ because pal::get_loaded_library is a templated helper. The C++ namespace members in fxr_resolver.cpp become thin wrappers around the new C entry points. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/CMakeLists.txt | 2 +- .../corehost/apphost/static/CMakeLists.txt | 1 + src/native/corehost/fxr_resolver.c | 423 ++++++++++++++++++ src/native/corehost/fxr_resolver.cpp | 257 ++--------- src/native/corehost/fxr_resolver.h | 50 ++- src/native/corehost/hostmisc/pal.h | 40 +- 6 files changed, 519 insertions(+), 254 deletions(-) create mode 100644 src/native/corehost/fxr_resolver.c diff --git a/src/native/corehost/CMakeLists.txt b/src/native/corehost/CMakeLists.txt index 07996e246de1ca..9be7532e2611cd 100644 --- a/src/native/corehost/CMakeLists.txt +++ b/src/native/corehost/CMakeLists.txt @@ -83,7 +83,7 @@ endif() if(NOT CLR_CMAKE_TARGET_BROWSER) add_library(fxr_resolver INTERFACE) - target_sources(fxr_resolver INTERFACE fxr_resolver.cpp) + target_sources(fxr_resolver INTERFACE fxr_resolver.cpp fxr_resolver.c) target_include_directories(fxr_resolver INTERFACE fxr) add_compile_definitions(RAPIDJSON_HAS_CXX17) diff --git a/src/native/corehost/apphost/static/CMakeLists.txt b/src/native/corehost/apphost/static/CMakeLists.txt index 3a973afd31be7c..0addec43ccd6eb 100644 --- a/src/native/corehost/apphost/static/CMakeLists.txt +++ b/src/native/corehost/apphost/static/CMakeLists.txt @@ -29,6 +29,7 @@ set(SOURCES ./hostpolicy_resolver.cpp ../../hostpolicy/static/coreclr_resolver.cpp ../../fxr_resolver.cpp + ../../fxr_resolver.c ../../corehost.cpp ) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c new file mode 100644 index 00000000000000..4a9fe66e7e991f --- /dev/null +++ b/src/native/corehost/fxr_resolver.c @@ -0,0 +1,423 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// C implementation of the fxr_resolver_* APIs (the C++ fxr_resolver:: namespace +// in fxr_resolver.cpp now delegates here). try_get_existing_fxr stays in C++ +// because pal::get_loaded_library is templated/typed for C++ callers. + +#include "fxr_resolver.h" + +#include "fx_ver.h" +#include "trace.h" +#include "utils.h" + +#include +#include + +// The default search set is app-local + environment variables + global install +// locations. Mirrors the C++ s_default_search. +#define FXR_SEARCH_LOCATION_DEFAULT_SET \ + (fxr_search_location_app_local \ + | fxr_search_location_environment_variable \ + | fxr_search_location_global) + +// Grow-style append. No-op if addition is NULL or empty (some PAL APIs return +// NULL to signal "not set"). *buf must be non-NULL on entry (caller seeds via +// pal_strdup or equivalent). Mirrors std::wstring::append in the C++ original: +// on OOM, realloc returns NULL and the subsequent memcpy crashes, which is the +// same observable outcome as a C++ std::bad_alloc terminate. +static void append_str(pal_char_t** buf, const pal_char_t* addition) +{ + if (addition == NULL || addition[0] == _X('\0')) + return; + + size_t cur_len = pal_strlen(*buf); + size_t add_len = pal_strlen(addition); + + *buf = (pal_char_t*)realloc(*buf, (cur_len + add_len + 1) * sizeof(pal_char_t)); + memcpy(*buf + cur_len, addition, (add_len + 1) * sizeof(pal_char_t)); +} + +typedef struct +{ + c_fx_ver_t max_ver; +} find_max_version_context_t; + +static bool find_max_version_callback(const pal_char_t* entry_name, void* ctx_in) +{ + find_max_version_context_t* ctx = (find_max_version_context_t*)ctx_in; + + trace_info(_X("Considering fxr version=[%s]..."), entry_name); + + c_fx_ver_t ver; + c_fx_ver_init(&ver); + if (!c_fx_ver_parse(entry_name, &ver, /*parse_only_production*/ false)) + { + c_fx_ver_cleanup(&ver); + return true; + } + + if (!c_fx_ver_is_empty(&ctx->max_ver) && c_fx_ver_compare(&ver, &ctx->max_ver) <= 0) + { + c_fx_ver_cleanup(&ver); + return true; + } + + c_fx_ver_cleanup(&ctx->max_ver); + ctx->max_ver = ver; // transfer ownership of pre/build + return true; +} + +// Find the latest version-numbered subdirectory of fxr_root that contains +// LIBFXR_NAME, and write its full file path to *out_fxr_path. On failure emits +// its own trace_error and returns false. +static bool get_latest_fxr(const pal_char_t* fxr_root, pal_char_t** out_fxr_path) +{ + trace_info(_X("Reading fx resolver directory=[%s]"), fxr_root); + + find_max_version_context_t ctx; + c_fx_ver_init(&ctx.max_ver); + + pal_readdir_onlydirectories(fxr_root, find_max_version_callback, &ctx); + + if (c_fx_ver_is_empty(&ctx.max_ver)) + { + trace_error(_X("Error: [%s] does not contain any version-numbered child folders"), fxr_root); + c_fx_ver_cleanup(&ctx.max_ver); + return false; + } + + pal_char_t max_ver_str[256]; + c_fx_ver_as_str(&ctx.max_ver, max_ver_str, ARRAY_SIZE(max_ver_str)); + c_fx_ver_cleanup(&ctx.max_ver); + + size_t fxr_dir_cap = pal_strlen(fxr_root) + pal_strlen(max_ver_str) + 2; // separator + NUL + pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); + fxr_dir[0] = _X('\0'); + utils_append_path(fxr_dir, fxr_dir_cap, fxr_root); + utils_append_path(fxr_dir, fxr_dir_cap, max_ver_str); + + trace_info(_X("Detected latest fxr version=[%s]..."), fxr_dir); + + *out_fxr_path = utils_file_exists_in_dir_alloc(fxr_dir, LIBFXR_NAME); + if (*out_fxr_path == NULL) + { + trace_error(_X("Error: the required library %s could not be found in [%s]"), LIBFXR_NAME, fxr_dir); + free(fxr_dir); + return false; + } + + trace_info(_X("Resolved fxr [%s]..."), *out_fxr_path); + free(fxr_dir); + return true; +} + +// Build the "you need to install .NET" diagnostic message and emit it via +// trace_error. Used when no fxr was found. Owns all of its allocations. +static void emit_missing_runtime_error( + const pal_char_t* root_path, + fxr_search_location search, + const pal_char_t* app_relative_dotnet_root, + const pal_char_t* env_var_name, + const pal_char_t* dotnet_root) +{ + bool search_app_local = (search & fxr_search_location_app_local) != 0; + bool search_app_relative = (search & fxr_search_location_app_relative) != 0 + && app_relative_dotnet_root != NULL && app_relative_dotnet_root[0] != _X('\0'); + bool search_env = (search & fxr_search_location_environment_variable) != 0; + bool search_global = (search & fxr_search_location_global) != 0; + + trace_verbose(_X("The required library %s could not be found. Search location options [0x%x]"), + LIBFXR_NAME, (unsigned int)search); + + pal_char_t* host_path = pal_get_own_executable_path(); + pal_char_t* location = pal_strdup(_X("Not found")); + pal_char_t* searched_locations = pal_strdup(_X("The following locations were searched:")); + pal_char_t* self_registered_dir = NULL; + pal_char_t* registered_config_location = NULL; + pal_char_t* default_install_location = NULL; + pal_char_t* download_url = NULL; + + if (search != FXR_SEARCH_LOCATION_DEFAULT_SET) + { + append_str(&location, _X(" - search options: [")); + if (search_app_local) append_str(&location, _X(" app_local")); + if (search_app_relative) append_str(&location, _X(" app_relative")); + if (search_env) append_str(&location, _X(" environment_variable")); + if (search_global) append_str(&location, _X(" global")); + append_str(&location, _X(" ]")); + + if (search_app_relative) + { + append_str(&location, _X(", app-relative path: ")); + append_str(&location, app_relative_dotnet_root); + } + } + + if (search_app_local && root_path != NULL && root_path[0] != _X('\0')) + { + append_str(&searched_locations, _X("\n Application directory:\n ")); + append_str(&searched_locations, root_path); + } + + if (search_app_relative) + { + append_str(&searched_locations, _X("\n App-relative location:\n ")); + append_str(&searched_locations, app_relative_dotnet_root); + } + + if (search_env) + { + append_str(&searched_locations, _X("\n Environment variable:\n ")); + if (env_var_name == NULL) + { + append_str(&searched_locations, DOTNET_ROOT_ARCH_ENV_VAR); + append_str(&searched_locations, _X(" = \n ")); + append_str(&searched_locations, DOTNET_ROOT_ENV_VAR _X(" = ")); + } + else + { + append_str(&searched_locations, env_var_name); + append_str(&searched_locations, _X(" = ")); + append_str(&searched_locations, dotnet_root); + } + } + + // Global locations are only listed if environment variables are not set. + if (search_global && env_var_name == NULL) + { + append_str(&searched_locations, _X("\n Registered location:\n ")); + registered_config_location = pal_get_dotnet_self_registered_config_location(); + append_str(&searched_locations, registered_config_location); + + self_registered_dir = pal_get_dotnet_self_registered_dir(); + bool self_registered_empty = self_registered_dir == NULL || self_registered_dir[0] == _X('\0'); + if (!self_registered_empty) + { + append_str(&searched_locations, _X(" = ")); + append_str(&searched_locations, self_registered_dir); + } + else + { + append_str(&searched_locations, _X(" = ")); + } + + // Default install location is only searched if self-registered location is not set. + if (self_registered_empty) + { + default_install_location = pal_get_default_installation_dir(); + append_str(&searched_locations, _X("\n Default location:\n ")); + append_str(&searched_locations, default_install_location); + } + } + + append_str(&location, _X("\n\n")); + append_str(&location, searched_locations); + + download_url = utils_get_download_url(NULL, NULL); + + trace_error( + MISSING_RUNTIME_ERROR_FORMAT, + INSTALL_NET_ERROR_MESSAGE, + host_path != NULL ? host_path : _X(""), + _STRINGIFY(CURRENT_ARCH_NAME), + _STRINGIFY(HOST_VERSION), + location, + download_url, + _STRINGIFY(HOST_VERSION)); + + free(host_path); + free(location); + free(searched_locations); + free(self_registered_dir); + free(registered_config_location); + free(default_install_location); + free(download_url); +} + +bool fxr_resolver_try_get_path( + const pal_char_t* root_path, + fxr_search_location search, + const pal_char_t* app_relative_dotnet_root, + pal_char_t** out_dotnet_root, + pal_char_t** out_fxr_path) +{ + *out_dotnet_root = NULL; + *out_fxr_path = NULL; + +#if defined(FEATURE_APPHOST) || defined(FEATURE_LIBHOST) + if (search == fxr_search_location_default) + search = FXR_SEARCH_LOCATION_DEFAULT_SET; + + bool search_app_local = (search & fxr_search_location_app_local) != 0; + bool search_app_relative = (search & fxr_search_location_app_relative) != 0 + && app_relative_dotnet_root != NULL && app_relative_dotnet_root[0] != _X('\0'); + bool search_env = (search & fxr_search_location_environment_variable) != 0; + bool search_global = (search & fxr_search_location_global) != 0; + + // For apphost and libhost, root_path is expected to be a directory. + // For libhost, it may be empty if app-local search is not desired (e.g. + // com/ijw/winrt hosts, nethost when no assembly path is specified). + // If a hostfxr exists in root_path, then assume self-contained. + if (search_app_local && root_path != NULL && root_path[0] != _X('\0')) + { + pal_char_t* app_local_fxr = utils_file_exists_in_dir_alloc(root_path, LIBFXR_NAME); + if (app_local_fxr != NULL) + { + trace_info(_X("Using app-local location [%s] as runtime location."), root_path); + trace_info(_X("Resolved fxr [%s]..."), app_local_fxr); + *out_dotnet_root = pal_strdup(root_path); + if (*out_dotnet_root == NULL) + { + free(app_local_fxr); + return false; + } + + *out_fxr_path = app_local_fxr; + return true; + } + } + + // Check in priority order (if specified by search location options): + // - App-relative .NET root location + // - Environment variables: DOTNET_ROOT_ and DOTNET_ROOT + // - Global installs: + // - self-registered install location + // - default install location + // Once a branch picks a dotnet_root, subsequent branches are skipped + // (dotnet_root != NULL is the cascade gate). + const pal_char_t* env_var_name = NULL; + pal_char_t* dotnet_root = NULL; + + if (search_app_relative) + { + pal_char_t* canonical_app_relative = pal_fullpath(app_relative_dotnet_root, /*skip_error_logging*/ false); + if (canonical_app_relative != NULL) + { + trace_info(_X("Using app-relative location [%s] as runtime location."), canonical_app_relative); + dotnet_root = pal_strdup(canonical_app_relative); + if (dotnet_root == NULL) + { + free(canonical_app_relative); + return false; + } + + pal_char_t* app_rel_fxr = utils_file_exists_in_dir_alloc(canonical_app_relative, LIBFXR_NAME); + free(canonical_app_relative); + if (app_rel_fxr != NULL) + { + trace_info(_X("Resolved fxr [%s]..."), app_rel_fxr); + *out_dotnet_root = dotnet_root; + *out_fxr_path = app_rel_fxr; + return true; + } + } + } + + if (dotnet_root == NULL && search_env) + { + if (utils_get_dotnet_root_from_env(&env_var_name, &dotnet_root)) + { + trace_info(_X("Using environment variable %s=[%s] as runtime location."), env_var_name, dotnet_root); + } + } + + if (dotnet_root == NULL && search_global) + { + pal_char_t* global = pal_get_dotnet_self_registered_dir(); + if (global == NULL) + global = pal_get_default_installation_dir(); + + if (global != NULL) + { + trace_info(_X("Using global install location [%s] as runtime location."), global); + dotnet_root = global; // transfer ownership + } + else + { + trace_error(_X("Error: the default install location cannot be obtained.")); + // env_var_name and dotnet_root are NULL on this branch. + return false; + } + } + + // If any branch picked a dotnet_root, try /host/fxr/. + if (dotnet_root != NULL) + { + size_t fxr_dir_cap = pal_strlen(dotnet_root) + STRING_LENGTH(_X("host")) + STRING_LENGTH(_X("fxr")) + 3; // 2 separators + NUL + pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); + fxr_dir[0] = _X('\0'); + utils_append_path(fxr_dir, fxr_dir_cap, dotnet_root); + utils_append_path(fxr_dir, fxr_dir_cap, _X("host")); + utils_append_path(fxr_dir, fxr_dir_cap, _X("fxr")); + + if (pal_directory_exists(fxr_dir)) + { + // get_latest_fxr emits its own trace_error on failure; do not fall + // through to the MISSING_RUNTIME_ERROR_FORMAT path. + pal_char_t* fxr_path = NULL; + bool ok = get_latest_fxr(fxr_dir, &fxr_path); + free(fxr_dir); + if (!ok) + { + free(dotnet_root); + return false; + } + + *out_dotnet_root = dotnet_root; + *out_fxr_path = fxr_path; + return true; + } + free(fxr_dir); + } + + emit_missing_runtime_error(root_path, search, app_relative_dotnet_root, + env_var_name, dotnet_root); + + free(dotnet_root); + return false; + +#else // !FEATURE_APPHOST && !FEATURE_LIBHOST + // Muxer: root_path is the full path to the host binary. + (void)search; + (void)app_relative_dotnet_root; + + pal_char_t* host_dir = utils_get_directory_alloc(root_path); + if (host_dir == NULL) + return false; + + if (!fxr_resolver_try_get_path_from_dotnet_root(host_dir, out_fxr_path)) + { + free(host_dir); + return false; + } + + *out_dotnet_root = host_dir; + return true; +#endif // !FEATURE_APPHOST && !FEATURE_LIBHOST +} + +bool fxr_resolver_try_get_path_from_dotnet_root( + const pal_char_t* dotnet_root, + pal_char_t** out_fxr_path) +{ + *out_fxr_path = NULL; + + size_t fxr_dir_cap = pal_strlen(dotnet_root) + STRING_LENGTH(_X("host")) + STRING_LENGTH(_X("fxr")) + 3; // 2 separators + NUL + pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); + fxr_dir[0] = _X('\0'); + utils_append_path(fxr_dir, fxr_dir_cap, dotnet_root); + utils_append_path(fxr_dir, fxr_dir_cap, _X("host")); + utils_append_path(fxr_dir, fxr_dir_cap, _X("fxr")); + + if (!pal_directory_exists(fxr_dir)) + { + trace_error(_X("Error: [%s] does not exist"), fxr_dir); + free(fxr_dir); + return false; + } + + bool ok = get_latest_fxr(fxr_dir, out_fxr_path); + free(fxr_dir); + return ok; +} diff --git a/src/native/corehost/fxr_resolver.cpp b/src/native/corehost/fxr_resolver.cpp index dbc1d625f8a14c..226051ffda8fbf 100644 --- a/src/native/corehost/fxr_resolver.cpp +++ b/src/native/corehost/fxr_resolver.cpp @@ -1,60 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#include +// C++ wrappers around the C fxr_resolver_* APIs in fxr_resolver.c. The C++ +// surface stays the same; each member just marshals between pal::string_t and +// pal_char_t*. try_get_existing_fxr remains a C++ function because +// pal::get_loaded_library is a templated helper that only has a C++ caller. + #include "fxr_resolver.h" -#include + +#include #include #include - -namespace -{ - bool get_latest_fxr(pal::string_t fxr_root, pal::string_t* out_fxr_path) - { - trace::info(_X("Reading fx resolver directory=[%s]"), fxr_root.c_str()); - - std::vector list; - pal::readdir_onlydirectories(fxr_root, &list); - - fx_ver_t max_ver; - for (const auto& dir : list) - { - trace::info(_X("Considering fxr version=[%s]..."), dir.c_str()); - - pal::string_t ver = get_filename(dir); - - fx_ver_t fx_ver; - if (fx_ver_t::parse(ver, &fx_ver, /* parse_only_production */ false)) - { - max_ver = std::max(max_ver, fx_ver); - } - } - - if (max_ver == fx_ver_t()) - { - trace::error(_X("Error: [%s] does not contain any version-numbered child folders"), fxr_root.c_str()); - return false; - } - - pal::string_t max_ver_str = max_ver.as_str(); - append_path(&fxr_root, max_ver_str.c_str()); - trace::info(_X("Detected latest fxr version=[%s]..."), fxr_root.c_str()); - - if (file_exists_in_dir(fxr_root, LIBFXR_NAME, out_fxr_path)) - { - trace::info(_X("Resolved fxr [%s]..."), out_fxr_path->c_str()); - return true; - } - - trace::error(_X("Error: the required library %s could not be found in [%s]"), LIBFXR_NAME, fxr_root.c_str()); - - return false; - } - - // The default is defined as app-local, environment variables, and global install locations - const fxr_resolver::search_location s_default_search = static_cast( - fxr_resolver::search_location_app_local | fxr_resolver::search_location_environment_variable | fxr_resolver::search_location_global); -} +#include bool fxr_resolver::try_get_path(const pal::string_t& root_path, pal::string_t* out_dotnet_root, pal::string_t* out_fxr_path) { @@ -68,191 +25,35 @@ bool fxr_resolver::try_get_path( /*out*/ pal::string_t* out_dotnet_root, /*out*/ pal::string_t* out_fxr_path) { -#if defined(FEATURE_APPHOST) || defined(FEATURE_LIBHOST) - if (search == search_location_default) - search = s_default_search; - - // For apphost and libhost, root_path is expected to be a directory. - // For libhost, it may be empty if app-local search is not desired (e.g. com/ijw/winrt hosts, nethost when no assembly path is specified) - // If a hostfxr exists in root_path, then assume self-contained. - bool search_app_local = (search & search_location_app_local) != 0; - if (search_app_local && root_path.length() > 0 && file_exists_in_dir(root_path, LIBFXR_NAME, out_fxr_path)) + pal_char_t* dotnet_root_c = nullptr; + pal_char_t* fxr_path_c = nullptr; + bool ok = ::fxr_resolver_try_get_path( + root_path.c_str(), + static_cast(search), + app_relative_dotnet_root != nullptr ? app_relative_dotnet_root->c_str() : nullptr, + &dotnet_root_c, + &fxr_path_c); + + if (ok) { - trace::info(_X("Using app-local location [%s] as runtime location."), root_path.c_str()); - trace::info(_X("Resolved fxr [%s]..."), out_fxr_path->c_str()); - out_dotnet_root->assign(root_path); - return true; + out_dotnet_root->assign(dotnet_root_c); + out_fxr_path->assign(fxr_path_c); } - // Check in priority order (if specified by search location options): - // - App-relative .NET root location - // - Environment variables: DOTNET_ROOT_ and DOTNET_ROOT - // - Global installs: - // - self-registered install location - // - default install location - bool search_app_relative = (search & search_location_app_relative) != 0 && app_relative_dotnet_root != nullptr && !app_relative_dotnet_root->empty(); - bool search_env = (search & search_location_environment_variable) != 0; - bool search_global = (search & search_location_global) != 0; - pal::string_t dotnet_root_env_var_name; - if (search_app_relative && pal::fullpath(app_relative_dotnet_root)) - { - trace::info(_X("Using app-relative location [%s] as runtime location."), app_relative_dotnet_root->c_str()); - out_dotnet_root->assign(*app_relative_dotnet_root); - if (file_exists_in_dir(*app_relative_dotnet_root, LIBFXR_NAME, out_fxr_path)) - { - trace::info(_X("Resolved fxr [%s]..."), out_fxr_path->c_str()); - return true; - } - } - else if (search_env && get_dotnet_root_from_env(&dotnet_root_env_var_name, out_dotnet_root)) - { - trace::info(_X("Using environment variable %s=[%s] as runtime location."), dotnet_root_env_var_name.c_str(), out_dotnet_root->c_str()); - } - else if (search_global) - { - pal::string_t global_install_location; - if (pal::get_dotnet_self_registered_dir(&global_install_location) || pal::get_default_installation_dir(&global_install_location)) - { - trace::info(_X("Using global install location [%s] as runtime location."), global_install_location.c_str()); - out_dotnet_root->assign(global_install_location); - } - else - { - trace::error(_X("Error: the default install location cannot be obtained.")); - return false; - } - } - - pal::string_t fxr_dir = *out_dotnet_root; - append_path(&fxr_dir, _X("host")); - append_path(&fxr_dir, _X("fxr")); - if (pal::directory_exists(fxr_dir)) - return get_latest_fxr(std::move(fxr_dir), out_fxr_path); - - // Failed to find hostfxr - trace::verbose(_X("The required library %s could not be found. Search location options [0x%x]"), LIBFXR_NAME, search); - - pal::string_t host_path; - pal::get_own_executable_path(&host_path); - - pal::string_t location = _X("Not found"); - if (search != s_default_search) - { - location.append(_X(" - search options: [")); - if (search_app_local) - location.append(_X(" app_local")); - - if (search_app_relative) - location.append(_X(" app_relative")); - - if (search_env) - location.append(_X(" environment_variable")); - - if (search_global) - location.append(_X(" global")); - - location.append(_X(" ]")); - if (search_app_relative) - { - location.append(_X(", app-relative path: ")); - location.append(app_relative_dotnet_root->c_str()); - } - } - - pal::string_t searched_locations = _X("The following locations were searched:"); - if (search_app_local && !root_path.empty()) - { - searched_locations.append(_X("\n Application directory:\n ")); - searched_locations.append(root_path); - } - - if (search_app_relative) - { - searched_locations.append(_X("\n App-relative location:\n ")); - searched_locations.append(*app_relative_dotnet_root); - } - - if (search_env) - { - searched_locations.append(_X("\n Environment variable:\n ")); - if (dotnet_root_env_var_name.empty()) - { - searched_locations.append(get_dotnet_root_env_var_for_arch(get_current_arch())); - searched_locations.append(_X(" = \n ")); - searched_locations.append(DOTNET_ROOT_ENV_VAR _X(" = ")); - } - else - { - searched_locations.append(dotnet_root_env_var_name); - searched_locations.append(_X(" = ")); - searched_locations.append(*out_dotnet_root); - } - } - - // Global locations are only searched if environment variables are not set - if (search_global && dotnet_root_env_var_name.empty()) - { - searched_locations.append(_X("\n Registered location:\n ")); - searched_locations.append(pal::get_dotnet_self_registered_config_location(get_current_arch())); - - pal::string_t self_registered_dir; - if (pal::get_dotnet_self_registered_dir(&self_registered_dir) && !self_registered_dir.empty()) - { - searched_locations.append(_X(" = ")); - searched_locations.append(self_registered_dir); - } - else - { - searched_locations.append(_X(" = ")); - } - - // Default install location is only searched if self-registered location is not set - if (self_registered_dir.empty()) - { - pal::string_t default_install_location; - pal::get_default_installation_dir(&default_install_location); - searched_locations.append(_X("\n Default location:\n ")); - searched_locations.append(default_install_location); - } - } - - location.append(_X("\n\n")); - location.append(searched_locations); - - trace::error( - MISSING_RUNTIME_ERROR_FORMAT, - INSTALL_NET_ERROR_MESSAGE, - host_path.c_str(), - get_current_arch_name(), - _STRINGIFY(HOST_VERSION), - location.c_str(), - get_download_url().c_str(), - _STRINGIFY(HOST_VERSION)); - return false; - -#else // !FEATURE_APPHOST && !FEATURE_LIBHOST - // For non-apphost and non-libhost (i.e. muxer), root_path is expected to be the full path to the host - pal::string_t host_dir; - host_dir.assign(get_directory(root_path)); - - out_dotnet_root->assign(host_dir); - - return fxr_resolver::try_get_path_from_dotnet_root(*out_dotnet_root, out_fxr_path); -#endif // !FEATURE_APPHOST && !FEATURE_LIBHOST + free(dotnet_root_c); + free(fxr_path_c); + return ok; } bool fxr_resolver::try_get_path_from_dotnet_root(const pal::string_t& dotnet_root, pal::string_t* out_fxr_path) { - pal::string_t fxr_dir = dotnet_root; - append_path(&fxr_dir, _X("host")); - append_path(&fxr_dir, _X("fxr")); - if (!pal::directory_exists(fxr_dir)) - { - trace::error(_X("Error: [%s] does not exist"), fxr_dir.c_str()); - return false; - } + pal_char_t* fxr_path_c = nullptr; + bool ok = ::fxr_resolver_try_get_path_from_dotnet_root(dotnet_root.c_str(), &fxr_path_c); + if (ok) + out_fxr_path->assign(fxr_path_c); - return get_latest_fxr(std::move(fxr_dir), out_fxr_path); + free(fxr_path_c); + return ok; } bool fxr_resolver::try_get_existing_fxr(pal::dll_t* out_fxr, pal::string_t* out_fxr_path) diff --git a/src/native/corehost/fxr_resolver.h b/src/native/corehost/fxr_resolver.h index 1703145059c9cf..0be9df5d3fb8ef 100644 --- a/src/native/corehost/fxr_resolver.h +++ b/src/native/corehost/fxr_resolver.h @@ -5,6 +5,44 @@ #define _COREHOST_CLI_FXR_RESOLVER_H_ #include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Keep in sync with fxr_resolver::search_location below and with +// DotNetRootOptions.SearchLocation in HostWriter.cs. +typedef enum +{ + fxr_search_location_default = 0, + fxr_search_location_app_local = 1 << 0, // Next to the app + fxr_search_location_app_relative = 1 << 1, // Path relative to the app read from the app binary + fxr_search_location_environment_variable = 1 << 2, // DOTNET_ROOT[_] environment variables + fxr_search_location_global = 1 << 3, // Registered and default global locations +} fxr_search_location; + +// Try to locate hostfxr using the specified search options. +// Caller should free() out_dotnet_root and out_fxr_path. +bool fxr_resolver_try_get_path( + const pal_char_t* root_path, + fxr_search_location search, + /*opt*/ const pal_char_t* app_relative_dotnet_root, + /*out*/ pal_char_t** out_dotnet_root, + /*out*/ pal_char_t** out_fxr_path); + +// Try to locate hostfxr at /host/fxr//hostfxr.. +// Caller should free() out_fxr_path. +bool fxr_resolver_try_get_path_from_dotnet_root( + const pal_char_t* dotnet_root, + /*out*/ pal_char_t** out_fxr_path); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + #include "hostfxr.h" #include "trace.h" #include "utils.h" @@ -15,11 +53,11 @@ namespace fxr_resolver // Keep in sync with DotNetRootOptions.SearchLocation in HostWriter.cs enum search_location : uint8_t { - search_location_default = 0, - search_location_app_local = 1 << 0, // Next to the app - search_location_app_relative = 1 << 1, // Path relative to the app read from the app binary - search_location_environment_variable = 1 << 2, // DOTNET_ROOT[_] environment variables - search_location_global = 1 << 3, // Registered and default global locations + search_location_default = fxr_search_location_default, + search_location_app_local = fxr_search_location_app_local, + search_location_app_relative = fxr_search_location_app_relative, + search_location_environment_variable = fxr_search_location_environment_variable, + search_location_global = fxr_search_location_global, }; bool try_get_path(const pal::string_t& root_path, pal::string_t* out_dotnet_root, pal::string_t* out_fxr_path); @@ -122,4 +160,6 @@ int load_fxr_and_get_delegate(hostfxr_delegate_type type, THostPathToConfigCallb } } +#endif // __cplusplus + #endif //_COREHOST_CLI_FXR_RESOLVER_H_ diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index fa67bf26d397c5..5081bbc86f04d7 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -200,6 +200,26 @@ pal_char_t* pal_get_dotnet_self_registered_config_location(void); } #endif +#if defined(TARGET_WINDOWS) +#define LIB_PREFIX "" +#define LIB_FILE_EXT ".dll" +#elif defined(TARGET_OSX) +#define LIB_PREFIX "lib" +#define LIB_FILE_EXT ".dylib" +#else +#define LIB_PREFIX "lib" +#define LIB_FILE_EXT ".so" +#endif + +#define LIB_NAME(NAME) LIB_PREFIX NAME +#define LIB_FILE_NAME(NAME) LIB_PREFIX NAME LIB_FILE_EXT +#define LIB_FILE_NAME_X(NAME) _STRINGIFY(LIB_FILE_NAME(NAME)) + +#define CORELIB_NAME _X("System.Private.CoreLib.dll") +#define LIBCORECLR_NAME LIB_FILE_NAME_X("coreclr") +#define LIBFXR_NAME LIB_FILE_NAME_X("hostfxr") +#define LIBHOSTPOLICY_NAME LIB_FILE_NAME_X("hostpolicy") + // ============================================================================ // C++ section (the original pal:: namespace surface) // ============================================================================ @@ -246,26 +266,6 @@ pal_char_t* pal_get_dotnet_self_registered_config_location(void); // degree of compat across their respective releases is usually high. // // We cannot maintain the same (compat) invariant for linux and thus, we will fallback to using lowest RID-Platform. -#if defined(TARGET_WINDOWS) -#define LIB_PREFIX "" -#define LIB_FILE_EXT ".dll" -#elif defined(TARGET_OSX) -#define LIB_PREFIX "lib" -#define LIB_FILE_EXT ".dylib" -#else -#define LIB_PREFIX "lib" -#define LIB_FILE_EXT ".so" -#endif - -#define LIB_NAME(NAME) LIB_PREFIX NAME -#define LIB_FILE_NAME(NAME) LIB_PREFIX NAME LIB_FILE_EXT -#define LIB_FILE_NAME_X(NAME) _STRINGIFY(LIB_FILE_NAME(NAME)) - -#define CORELIB_NAME _X("System.Private.CoreLib.dll") -#define LIBCORECLR_NAME LIB_FILE_NAME_X("coreclr") -#define LIBFXR_NAME LIB_FILE_NAME_X("hostfxr") -#define LIBHOSTPOLICY_NAME LIB_FILE_NAME_X("hostpolicy") - #if !defined(PATH_MAX) && !defined(_WIN32) #define PATH_MAX 4096 #endif From 3f9d1753996c162a2dba4efb4f2e64c2b9c45de6 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 5 Jun 2026 11:07:15 -0700 Subject: [PATCH 05/14] Simplify emit_missing_runtime_error message build Replace the grow-style append_str helper (repeated realloc) with a stack-allocated array of string fragments that is concatenated in a single pass. The size and copy loops walk the parts array once each, giving a clear three-phase structure: collect parts -> compute size -> copy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/fxr_resolver.c | 118 +++++++++++++++-------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index 4a9fe66e7e991f..cbcf8d131fda77 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -13,6 +13,7 @@ #include #include +#include // The default search set is app-local + environment variables + global install // locations. Mirrors the C++ s_default_search. @@ -21,23 +22,6 @@ | fxr_search_location_environment_variable \ | fxr_search_location_global) -// Grow-style append. No-op if addition is NULL or empty (some PAL APIs return -// NULL to signal "not set"). *buf must be non-NULL on entry (caller seeds via -// pal_strdup or equivalent). Mirrors std::wstring::append in the C++ original: -// on OOM, realloc returns NULL and the subsequent memcpy crashes, which is the -// same observable outcome as a C++ std::bad_alloc terminate. -static void append_str(pal_char_t** buf, const pal_char_t* addition) -{ - if (addition == NULL || addition[0] == _X('\0')) - return; - - size_t cur_len = pal_strlen(*buf); - size_t add_len = pal_strlen(addition); - - *buf = (pal_char_t*)realloc(*buf, (cur_len + add_len + 1) * sizeof(pal_char_t)); - memcpy(*buf + cur_len, addition, (add_len + 1) * sizeof(pal_char_t)); -} - typedef struct { c_fx_ver_t max_ver; @@ -131,88 +115,113 @@ static void emit_missing_runtime_error( LIBFXR_NAME, (unsigned int)search); pal_char_t* host_path = pal_get_own_executable_path(); - pal_char_t* location = pal_strdup(_X("Not found")); - pal_char_t* searched_locations = pal_strdup(_X("The following locations were searched:")); pal_char_t* self_registered_dir = NULL; pal_char_t* registered_config_location = NULL; pal_char_t* default_install_location = NULL; pal_char_t* download_url = NULL; + // Build the message as an array of string fragments, then concatenate once + // at the end. Current upper bound is ~24 message_parts; 32 is comfortable. + const pal_char_t* message_parts[32]; + size_t index = 0; + + message_parts[index++] = _X("Not found"); if (search != FXR_SEARCH_LOCATION_DEFAULT_SET) { - append_str(&location, _X(" - search options: [")); - if (search_app_local) append_str(&location, _X(" app_local")); - if (search_app_relative) append_str(&location, _X(" app_relative")); - if (search_env) append_str(&location, _X(" environment_variable")); - if (search_global) append_str(&location, _X(" global")); - append_str(&location, _X(" ]")); + message_parts[index++] = _X(" - search options: ["); + + if (search_app_local) + message_parts[index++] = _X(" app_local"); + + if (search_app_relative) + message_parts[index++] = _X(" app_relative"); + + if (search_env) + message_parts[index++] = _X(" environment_variable"); + + if (search_global) + message_parts[index++] = _X(" global"); + + message_parts[index++] = _X(" ]"); if (search_app_relative) { - append_str(&location, _X(", app-relative path: ")); - append_str(&location, app_relative_dotnet_root); + message_parts[index++] = _X(", app-relative path: "); + message_parts[index++] = app_relative_dotnet_root; } } + message_parts[index++] = _X("\n\nThe following locations were searched:"); + if (search_app_local && root_path != NULL && root_path[0] != _X('\0')) { - append_str(&searched_locations, _X("\n Application directory:\n ")); - append_str(&searched_locations, root_path); + message_parts[index++] = _X("\n Application directory:\n "); + message_parts[index++] = root_path; } if (search_app_relative) { - append_str(&searched_locations, _X("\n App-relative location:\n ")); - append_str(&searched_locations, app_relative_dotnet_root); + message_parts[index++] = _X("\n App-relative location:\n "); + message_parts[index++] = app_relative_dotnet_root; } if (search_env) { - append_str(&searched_locations, _X("\n Environment variable:\n ")); + message_parts[index++] = _X("\n Environment variable:\n "); if (env_var_name == NULL) { - append_str(&searched_locations, DOTNET_ROOT_ARCH_ENV_VAR); - append_str(&searched_locations, _X(" = \n ")); - append_str(&searched_locations, DOTNET_ROOT_ENV_VAR _X(" = ")); + message_parts[index++] = DOTNET_ROOT_ARCH_ENV_VAR _X(" = \n ") + DOTNET_ROOT_ENV_VAR _X(" = "); } else { - append_str(&searched_locations, env_var_name); - append_str(&searched_locations, _X(" = ")); - append_str(&searched_locations, dotnet_root); + message_parts[index++] = env_var_name; + message_parts[index++] = _X(" = "); + message_parts[index++] = dotnet_root; } } // Global locations are only listed if environment variables are not set. if (search_global && env_var_name == NULL) { - append_str(&searched_locations, _X("\n Registered location:\n ")); registered_config_location = pal_get_dotnet_self_registered_config_location(); - append_str(&searched_locations, registered_config_location); - self_registered_dir = pal_get_dotnet_self_registered_dir(); bool self_registered_empty = self_registered_dir == NULL || self_registered_dir[0] == _X('\0'); - if (!self_registered_empty) - { - append_str(&searched_locations, _X(" = ")); - append_str(&searched_locations, self_registered_dir); - } - else - { - append_str(&searched_locations, _X(" = ")); - } + + message_parts[index++] = _X("\n Registered location:\n "); + message_parts[index++] = registered_config_location; + message_parts[index++] = _X(" = "); + message_parts[index++] = self_registered_empty ? _X("") : self_registered_dir; // Default install location is only searched if self-registered location is not set. if (self_registered_empty) { default_install_location = pal_get_default_installation_dir(); - append_str(&searched_locations, _X("\n Default location:\n ")); - append_str(&searched_locations, default_install_location); + if (default_install_location != NULL) + { + message_parts[index++] = _X("\n Default location:\n "); + message_parts[index++] = default_install_location; + } } } - append_str(&location, _X("\n\n")); - append_str(&location, searched_locations); + assert(index <= ARRAY_SIZE(message_parts)); + + size_t total = 0; + for (size_t i = 0; i < index; ++i) + { + total += pal_strlen(message_parts[i]); + } + + pal_char_t* location = (pal_char_t*)malloc((total + 1) * sizeof(pal_char_t)); + pal_char_t* dst = location; + for (size_t i = 0; i < index; ++i) + { + size_t len = pal_strlen(message_parts[i]); + memcpy(dst, message_parts[i], len * sizeof(pal_char_t)); + dst += len; + } + *dst = _X('\0'); download_url = utils_get_download_url(NULL, NULL); @@ -228,7 +237,6 @@ static void emit_missing_runtime_error( free(host_path); free(location); - free(searched_locations); free(self_registered_dir); free(registered_config_location); free(default_install_location); From cafcda1e0a02e80c5783bb02cf4334eb59a9d09a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 5 Jun 2026 14:41:37 -0700 Subject: [PATCH 06/14] Simplify hostmisc C helpers and tidy fxr_resolver pal.unix.c install-location helpers: - Inline compose_install_location_file_path into its only caller. - Simplify pal_get_dotnet_self_registered_dir: in-place truncation of the "_" suffix, single cleanup path, rename `ok` to `success`, assert that file_found is true on success. - Remove ascii_lower_dup helper. - Drop private join_path_alloc in favor of utils_append_path_alloc. pal.windows.c registry helpers: - get_dotnet_install_location_registry_path and format_registry_path use pal_str_printf instead of hand-rolled memcpy concatenation. - get_dotnet_install_location_registry_path inlines the suffix into the format string and uses STRING_LENGTH for the compile-time size, matching format_registry_path's pattern. pal_char_t string ops: - Use pal_strlen instead of bare wcslen/strlen on pal_char_t* values in pal.windows.c and pal.unix.c. utils: - Add utils_append_path_alloc (delegates to utils_append_path). - Rename utils_file_exists_in_dir_alloc -> utils_find_file_in_dir and utils_get_directory_alloc -> utils_get_directory (no non-alloc twin). - Change utils_get_filename to return void; sole caller already used the empty-string sentinel for failure. - Rename utils_append_path parameters to path_buffer/path_buffer_len. - Add asserts on the overflow paths in utils_append_path and utils_get_filename. - utils_append_path: merge the empty-buffer special case into the unified flow (single overflow check, no early return). - utils_get_directory: build the result up front and use pal_strrchr on the working copy after trimming trailing separators. - utils_get_file_path_from_env: rename the env getenv result to env_value and the canonicalized path to file_path. fxr_resolver.c: - Rename emit_missing_runtime_error -> print_missing_runtime_error. Doc cleanups: - "NUL-terminated" -> "null-terminated" across pal.h, trace.c, utils.h, and an inline comment in utils.c. - Rewrite the doc comment on get_dotnet_install_location_registry_path in pal.windows.c. - Trim a few obvious comments in utils.h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/fxr_resolver.c | 60 +++--- src/native/corehost/hostmisc/pal.h | 10 +- src/native/corehost/hostmisc/pal.unix.c | 201 ++++----------------- src/native/corehost/hostmisc/pal.windows.c | 77 ++------ src/native/corehost/hostmisc/trace.c | 2 +- src/native/corehost/hostmisc/utils.c | 126 +++++++------ src/native/corehost/hostmisc/utils.cpp | 4 +- src/native/corehost/hostmisc/utils.h | 31 ++-- 8 files changed, 174 insertions(+), 337 deletions(-) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index cbcf8d131fda77..0999c9472860b2 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -16,7 +16,6 @@ #include // The default search set is app-local + environment variables + global install -// locations. Mirrors the C++ s_default_search. #define FXR_SEARCH_LOCATION_DEFAULT_SET \ (fxr_search_location_app_local \ | fxr_search_location_environment_variable \ @@ -75,15 +74,13 @@ static bool get_latest_fxr(const pal_char_t* fxr_root, pal_char_t** out_fxr_path c_fx_ver_as_str(&ctx.max_ver, max_ver_str, ARRAY_SIZE(max_ver_str)); c_fx_ver_cleanup(&ctx.max_ver); - size_t fxr_dir_cap = pal_strlen(fxr_root) + pal_strlen(max_ver_str) + 2; // separator + NUL - pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); - fxr_dir[0] = _X('\0'); - utils_append_path(fxr_dir, fxr_dir_cap, fxr_root); - utils_append_path(fxr_dir, fxr_dir_cap, max_ver_str); + pal_char_t* fxr_dir = utils_append_path_alloc(fxr_root, max_ver_str); + if (fxr_dir == NULL) + return false; trace_info(_X("Detected latest fxr version=[%s]..."), fxr_dir); - *out_fxr_path = utils_file_exists_in_dir_alloc(fxr_dir, LIBFXR_NAME); + *out_fxr_path = utils_find_file_in_dir(fxr_dir, LIBFXR_NAME); if (*out_fxr_path == NULL) { trace_error(_X("Error: the required library %s could not be found in [%s]"), LIBFXR_NAME, fxr_dir); @@ -96,9 +93,23 @@ static bool get_latest_fxr(const pal_char_t* fxr_root, pal_char_t** out_fxr_path return true; } -// Build the "you need to install .NET" diagnostic message and emit it via -// trace_error. Used when no fxr was found. Owns all of its allocations. -static void emit_missing_runtime_error( +// Build "/host/fxr". +// Caller should free() the returned pointer. +static pal_char_t* get_fxr_dir(const pal_char_t* dotnet_root) +{ + size_t cap = pal_strlen(dotnet_root) + STRING_LENGTH(_X("host")) + STRING_LENGTH(_X("fxr")) + 3; // 2 separators + NUL + pal_char_t* fxr_dir = (pal_char_t*)malloc(cap * sizeof(pal_char_t)); + if (fxr_dir == NULL) + return NULL; + + fxr_dir[0] = _X('\0'); + utils_append_path(fxr_dir, cap, dotnet_root); + utils_append_path(fxr_dir, cap, _X("host")); + utils_append_path(fxr_dir, cap, _X("fxr")); + return fxr_dir; +} + +static void print_missing_runtime_error( const pal_char_t* root_path, fxr_search_location search, const pal_char_t* app_relative_dotnet_root, @@ -269,7 +280,7 @@ bool fxr_resolver_try_get_path( // If a hostfxr exists in root_path, then assume self-contained. if (search_app_local && root_path != NULL && root_path[0] != _X('\0')) { - pal_char_t* app_local_fxr = utils_file_exists_in_dir_alloc(root_path, LIBFXR_NAME); + pal_char_t* app_local_fxr = utils_find_file_in_dir(root_path, LIBFXR_NAME); if (app_local_fxr != NULL) { trace_info(_X("Using app-local location [%s] as runtime location."), root_path); @@ -310,7 +321,7 @@ bool fxr_resolver_try_get_path( return false; } - pal_char_t* app_rel_fxr = utils_file_exists_in_dir_alloc(canonical_app_relative, LIBFXR_NAME); + pal_char_t* app_rel_fxr = utils_find_file_in_dir(canonical_app_relative, LIBFXR_NAME); free(canonical_app_relative); if (app_rel_fxr != NULL) { @@ -352,12 +363,12 @@ bool fxr_resolver_try_get_path( // If any branch picked a dotnet_root, try /host/fxr/. if (dotnet_root != NULL) { - size_t fxr_dir_cap = pal_strlen(dotnet_root) + STRING_LENGTH(_X("host")) + STRING_LENGTH(_X("fxr")) + 3; // 2 separators + NUL - pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); - fxr_dir[0] = _X('\0'); - utils_append_path(fxr_dir, fxr_dir_cap, dotnet_root); - utils_append_path(fxr_dir, fxr_dir_cap, _X("host")); - utils_append_path(fxr_dir, fxr_dir_cap, _X("fxr")); + pal_char_t* fxr_dir = get_fxr_dir(dotnet_root); + if (fxr_dir == NULL) + { + free(dotnet_root); + return false; + } if (pal_directory_exists(fxr_dir)) { @@ -379,7 +390,7 @@ bool fxr_resolver_try_get_path( free(fxr_dir); } - emit_missing_runtime_error(root_path, search, app_relative_dotnet_root, + print_missing_runtime_error(root_path, search, app_relative_dotnet_root, env_var_name, dotnet_root); free(dotnet_root); @@ -390,7 +401,7 @@ bool fxr_resolver_try_get_path( (void)search; (void)app_relative_dotnet_root; - pal_char_t* host_dir = utils_get_directory_alloc(root_path); + pal_char_t* host_dir = utils_get_directory(root_path); if (host_dir == NULL) return false; @@ -411,12 +422,9 @@ bool fxr_resolver_try_get_path_from_dotnet_root( { *out_fxr_path = NULL; - size_t fxr_dir_cap = pal_strlen(dotnet_root) + STRING_LENGTH(_X("host")) + STRING_LENGTH(_X("fxr")) + 3; // 2 separators + NUL - pal_char_t* fxr_dir = (pal_char_t*)malloc(fxr_dir_cap * sizeof(pal_char_t)); - fxr_dir[0] = _X('\0'); - utils_append_path(fxr_dir, fxr_dir_cap, dotnet_root); - utils_append_path(fxr_dir, fxr_dir_cap, _X("host")); - utils_append_path(fxr_dir, fxr_dir_cap, _X("fxr")); + pal_char_t* fxr_dir = get_fxr_dir(dotnet_root); + if (fxr_dir == NULL) + return false; if (!pal_directory_exists(fxr_dir)) { diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 5081bbc86f04d7..4656bb335de09b 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -123,7 +123,7 @@ extern "C" { // pal_char_t-based C-callable APIs. -// Returns a heap-allocated, NUL-terminated copy of the current process's +// Returns a heap-allocated, null-terminated copy of the current process's // executable path, or NULL on failure. Caller must free() the returned pointer. pal_char_t* pal_get_own_executable_path(void); @@ -132,17 +132,17 @@ bool pal_directory_exists(const pal_char_t* path); // Returns true if the file or directory exists. Equivalent to pal::file_exists. bool pal_file_exists(const pal_char_t* path); -// Returns a heap-allocated, NUL-terminated copy of the given string, or +// Returns a heap-allocated, null-terminated copy of the given string, or // NULL on allocation failure. Caller should free() the returned pointer. pal_char_t* pal_strdup(const pal_char_t* str); -// Returns a heap-allocated, NUL-terminated copy of the named environment +// Returns a heap-allocated, null-terminated copy of the named environment // variable's value, or NULL if the variable is unset or set to the empty // string. Caller must free() the returned pointer. pal_char_t* pal_getenv(const pal_char_t* name); // Duplicate the first `len` pal_char_t characters of `src` into a -// heap-allocated, NUL-terminated buffer. Returns NULL on allocation failure. +// heap-allocated, null-terminated buffer. Returns NULL on allocation failure. static inline pal_char_t* pal_strndup(const pal_char_t* src, size_t len) { pal_char_t* buf = (pal_char_t*)malloc((len + 1) * sizeof(pal_char_t)); @@ -155,7 +155,7 @@ static inline pal_char_t* pal_strndup(const pal_char_t* src, size_t len) } // Canonicalize a path and verify it exists. Returns a heap-allocated, -// NUL-terminated canonical path, or NULL on failure. Caller should free() +// null-terminated canonical path, or NULL on failure. Caller should free() // the returned pointer. When skip_error_logging is true, failure-path // trace messages are suppressed (used when probing for optional files). pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging); diff --git a/src/native/corehost/hostmisc/pal.unix.c b/src/native/corehost/hostmisc/pal.unix.c index e43d324ef7d670..86ecbb3cc40c67 100644 --- a/src/native/corehost/hostmisc/pal.unix.c +++ b/src/native/corehost/hostmisc/pal.unix.c @@ -3,10 +3,15 @@ // C implementations of the pal_* APIs needed by trace.c on non-Windows. +#if defined(TARGET_FREEBSD) +#define _WITH_GETLINE +#endif + #include "pal.h" #include "trace.h" #include "utils.h" +#include #include #include #include @@ -61,6 +66,7 @@ pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) { if (errno != ENOENT && !skip_error_logging) trace_error(_X("realpath(%s) failed: %s"), path, strerror(errno)); + return NULL; } @@ -153,84 +159,25 @@ bool pal_is_emulating_x64(void) return is_translated_process == 1; } -// ASCII-only lowercase of `src` into a fresh allocation. Used to lower-case -// architecture name segments embedded into file paths (matches the C++ side's -// defensive to_lower() over arch names). -static pal_char_t* ascii_lower_dup(const pal_char_t* src) -{ - if (src == NULL) - return NULL; - - size_t len = strlen(src); - pal_char_t* dup = (pal_char_t*)malloc(len + 1); - if (dup == NULL) - return NULL; - - for (size_t i = 0; i < len; ++i) - { - pal_char_t c = src[i]; - dup[i] = (c >= 'A' && c <= 'Z') ? (pal_char_t)(c + ('a' - 'A')) : c; - } - dup[len] = '\0'; - return dup; -} - -// Allocates the concatenation of `dir + '/' + leaf`. Returns NULL on -// allocation failure. -static pal_char_t* join_path_alloc(const pal_char_t* dir, const pal_char_t* leaf) -{ - size_t dir_len = strlen(dir); - size_t leaf_len = strlen(leaf); - bool need_sep = dir_len > 0 && dir[dir_len - 1] != '/'; - size_t total = dir_len + (need_sep ? 1 : 0) + leaf_len + 1; - - pal_char_t* out = (pal_char_t*)malloc(total); - if (out == NULL) - return NULL; - - memcpy(out, dir, dir_len); - if (need_sep) - out[dir_len] = '/'; - memcpy(out + dir_len + (need_sep ? 1 : 0), leaf, leaf_len); - out[total - 1] = '\0'; - return out; -} - // Reads up to the first newline from `file`, returning the line (with the -// trailing '\n' stripped) as a fresh allocation in *out_line. -// Returns true only if a non-empty line was read. Mirrors the C++ -// `get_line_from_file` semantics: blank lines are NOT skipped, and an empty -// line (or pure-EOF) returns false. +// trailing '\n' stripped) as a fresh allocation in *out_line. Blank lines are +// not skipped; an empty line (or pure EOF) returns false, so the function +// returns true only when a non-empty line was read. static bool get_line_from_file(FILE* file, pal_char_t** out_line) { *out_line = NULL; - char buffer[256]; pal_char_t* line = NULL; - size_t line_len = 0; + size_t capacity = 0; + ssize_t len = getline(&line, &capacity, file); - while (fgets(buffer, sizeof(buffer), file)) - { - size_t chunk_len = strlen(buffer); - pal_char_t* grown = (pal_char_t*)realloc(line, line_len + chunk_len + 1); - if (grown == NULL) - { - free(line); - return false; - } - line = grown; - memcpy(line + line_len, buffer, chunk_len); - line_len += chunk_len; - line[line_len] = '\0'; - - if (line_len > 0 && line[line_len - 1] == '\n') - { - line[--line_len] = '\0'; - break; - } - } + // getline keeps the trailing newline; strip it. + if (len > 0 && line[len - 1] == '\n') + line[--len] = '\0'; - if (line == NULL || line_len == 0) + // Reject EOF/error (len < 0) and blank lines (len == 0). getline may + // allocate the buffer even on failure, so free it either way. + if (len <= 0) { free(line); return false; @@ -277,57 +224,13 @@ static bool get_install_location_from_file(const pal_char_t* file_path, bool* ou return true; } -// Computes "/install_location_". `base` defaults to -// "/etc/dotnet" but is overridable by _DOTNET_TEST_INSTALL_LOCATION_PATH. -static pal_char_t* compose_install_location_file_path(void) -{ - pal_char_t* base = utils_test_only_getenv(_X("_DOTNET_TEST_INSTALL_LOCATION_PATH")); - if (base == NULL) - { - base = pal_strdup(_X("/etc/dotnet")); - if (base == NULL) - return NULL; - } - - pal_char_t* arch_lower = ascii_lower_dup(_STRINGIFY(CURRENT_ARCH_NAME)); - if (arch_lower == NULL) - { - free(base); - return NULL; - } - - size_t base_len = strlen(base); - bool need_sep = base_len > 0 && base[base_len - 1] != '/'; - const char prefix[] = "install_location_"; - size_t prefix_len = ARRAY_SIZE(prefix) - 1; - size_t arch_len = strlen(arch_lower); - size_t total = base_len + (need_sep ? 1 : 0) + prefix_len + arch_len + 1; - - pal_char_t* out = (pal_char_t*)malloc(total); - if (out == NULL) - { - free(base); - free(arch_lower); - return NULL; - } - - memcpy(out, base, base_len); - size_t pos = base_len; - if (need_sep) - out[pos++] = '/'; - memcpy(out + pos, prefix, prefix_len); - pos += prefix_len; - memcpy(out + pos, arch_lower, arch_len); - out[total - 1] = '\0'; - - free(base); - free(arch_lower); - return out; -} - pal_char_t* pal_get_dotnet_self_registered_config_location(void) { - return compose_install_location_file_path(); + pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_INSTALL_LOCATION_PATH")); + const pal_char_t* base = override != NULL ? override : _X("/etc/dotnet"); + pal_char_t* result = utils_append_path_alloc(base, _X("install_location_") _STRINGIFY(CURRENT_ARCH_NAME)); + free(override); + return result; } pal_char_t* pal_get_dotnet_self_registered_dir(void) @@ -336,57 +239,31 @@ pal_char_t* pal_get_dotnet_self_registered_dir(void) if (override != NULL) return override; - pal_char_t* arch_path = compose_install_location_file_path(); - if (arch_path == NULL) + pal_char_t* path = pal_get_dotnet_self_registered_config_location(); + if (path == NULL) return NULL; - trace_verbose(_X("Looking for architecture-specific install_location file in '%s'."), arch_path); + trace_verbose(_X("Looking for architecture-specific install_location file in '%s'."), path); pal_char_t* location = NULL; bool file_found = false; - bool ok = get_install_location_from_file(arch_path, &file_found, &location); - if (!ok && !file_found) + bool success = get_install_location_from_file(path, &file_found, &location); + if (!success && !file_found) { - // For current architecture, fall back to the non-arch-specific file - // in the same base directory. - char* sep = strrchr(arch_path, '/'); - pal_char_t* legacy = NULL; - if (sep != NULL) - { - size_t base_len = (size_t)(sep - arch_path); - pal_char_t* base = (pal_char_t*)malloc(base_len + 1); - if (base != NULL) - { - memcpy(base, arch_path, base_len); - base[base_len] = '\0'; - legacy = join_path_alloc(base, _X("install_location")); - free(base); - } - } - else - { - legacy = pal_strdup(_X("install_location")); - } + // Fall back to the non-arch-specific file in the same directory: + // install_location instead of install_location_. + path[pal_strlen(path) - (sizeof(_X("_") _STRINGIFY(CURRENT_ARCH_NAME)) - 1)] = '\0'; - free(arch_path); - arch_path = NULL; + trace_verbose(_X("Looking for install_location file in '%s'."), path); + success = get_install_location_from_file(path, &file_found, &location); + } - if (legacy == NULL) - return NULL; + free(path); - trace_verbose(_X("Looking for install_location file in '%s'."), legacy); - ok = get_install_location_from_file(legacy, &file_found, &location); - free(legacy); - if (!ok) - return NULL; - } - else - { - free(arch_path); - if (!ok) - return NULL; - } + if (!success) + return NULL; + assert(file_found); trace_verbose(_X("Found registered install location '%s'."), location); return location; } @@ -401,7 +278,7 @@ pal_char_t* pal_get_default_installation_dir(void) const pal_char_t* base = _X("/usr/local/share/dotnet"); if (pal_is_emulating_x64()) { - return join_path_alloc(base, _STRINGIFY(CURRENT_ARCH_NAME)); + return utils_append_path_alloc(base, _STRINGIFY(CURRENT_ARCH_NAME)); } return pal_strdup(base); @@ -414,7 +291,7 @@ pal_char_t* pal_get_default_installation_dir(void) mib[1] = USER_LOCALBASE; if (sysctl(mib, 2, buf, &len, NULL, 0) == 0) { - return join_path_alloc(buf, _X("share/dotnet")); + return utils_append_path_alloc(buf, _X("share/dotnet")); } return pal_strdup(_X("/usr/local/share/dotnet")); diff --git a/src/native/corehost/hostmisc/pal.windows.c b/src/native/corehost/hostmisc/pal.windows.c index fbd5946ff4ceba..88cab942bc36ac 100644 --- a/src/native/corehost/hostmisc/pal.windows.c +++ b/src/native/corehost/hostmisc/pal.windows.c @@ -116,7 +116,6 @@ pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) WIN32_FILE_ATTRIBUTE_DATA data; if (GetFileAttributesExW(path, GetFileExInfoStandard, &data) != 0) return pal_strdup(path); - // Fall through and let GetFullPathNameW have a chance, matching C++ pal::fullpath. } // Start with a MAX_PATH-sized buffer; the typical case fits. @@ -180,9 +179,8 @@ pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) bool pal_file_exists(const pal_char_t* path) { - // Matches the C++ pal::file_exists semantics: canonicalize and verify - // existence (with logging suppressed). This handles long paths via the - // \\?\ prefix machinery in pal_fullpath. + // pal_fullpath canonicalizes (adding the long-path \\?\ prefix when needed) + // and verifies existence, so a non-NULL result means the path exists. pal_char_t* resolved = pal_fullpath(path, true); bool exists = resolved != NULL; free(resolved); @@ -196,7 +194,7 @@ bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t // Build the search string: path + "\\*". One extra char beyond path // length is needed for the separator if path doesn't already end with one. - size_t path_len = wcslen(path); + size_t path_len = pal_strlen(path); size_t search_len = path_len + 3; // worst case: '\\', '*', NUL pal_char_t* search = (pal_char_t*)malloc(search_len * sizeof(pal_char_t)); if (search == NULL) @@ -274,89 +272,52 @@ bool pal_is_emulating_x64(void) #endif } -// Resolves the registry hive and sub-key path consulted to discover the -// globally-registered .NET install for the current architecture. The value -// name is always "InstallLocation" (REG_SZ). -// Honors _DOTNET_TEST_REGISTRY_PATH to replace the default SOFTWARE\dotnet -// base path; if the override begins with the literal "HKEY_CURRENT_USER\" -// (case-sensitive), the hive is switched to HKCU and the prefix is stripped. -// Returns true on success; on failure *out_sub_key is NULL. +// Get the registry hive and sub-key for the globally-registered .NET install: +// HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\. +// If set, the _DOTNET_TEST_REGISTRY_PATH environment variable can switch the +// hive to HKCU and change the SOFTWARE\dotnet part of the sub-key. static bool get_dotnet_install_location_registry_path(HKEY* out_hive, pal_char_t** out_sub_key) { *out_hive = HKEY_LOCAL_MACHINE; - *out_sub_key = NULL; - pal_char_t* base = NULL; + const pal_char_t* base = _X("SOFTWARE\\dotnet"); pal_char_t* override = utils_test_only_getenv(_X("_DOTNET_TEST_REGISTRY_PATH")); if (override != NULL) { const pal_char_t hkcu_prefix[] = _X("HKEY_CURRENT_USER\\"); - const size_t hkcu_prefix_len = ARRAY_SIZE(hkcu_prefix) - 1; - if (wcsncmp(override, hkcu_prefix, hkcu_prefix_len) == 0) + if (wcsncmp(override, hkcu_prefix, ARRAY_SIZE(hkcu_prefix) - 1) == 0) { *out_hive = HKEY_CURRENT_USER; - base = pal_strdup(override + hkcu_prefix_len); + base = override + (ARRAY_SIZE(hkcu_prefix) - 1); } else { base = override; - override = NULL; } - - free(override); - if (base == NULL) - return false; - } - else - { - base = pal_strdup(_X("SOFTWARE\\dotnet")); - if (base == NULL) - return false; } - const pal_char_t suffix[] = _X("\\Setup\\InstalledVersions\\"); - const pal_char_t* arch = _STRINGIFY(CURRENT_ARCH_NAME); - size_t base_len = wcslen(base); - size_t suffix_len = ARRAY_SIZE(suffix) - 1; - size_t arch_len = wcslen(arch); - size_t total = base_len + suffix_len + arch_len + 1; + size_t total = pal_strlen(base) + STRING_LENGTH("\\Setup\\InstalledVersions\\" CURRENT_ARCH_NAME) + 1; pal_char_t* combined = (pal_char_t*)malloc(total * sizeof(pal_char_t)); - if (combined == NULL) - { - free(base); - return false; - } - - memcpy(combined, base, base_len * sizeof(pal_char_t)); - memcpy(combined + base_len, suffix, suffix_len * sizeof(pal_char_t)); - memcpy(combined + base_len + suffix_len, arch, arch_len * sizeof(pal_char_t)); - combined[total - 1] = L'\0'; + if (combined != NULL) + pal_str_printf(combined, total, _X("%s\\Setup\\InstalledVersions\\") _STRINGIFY(CURRENT_ARCH_NAME), base); - free(base); + free(override); *out_sub_key = combined; - return true; + return combined != NULL; } // Allocates "HKLM\\InstallLocation" or "HKCU\..." for display/tracing. static pal_char_t* format_registry_path(HKEY hive, const pal_char_t* sub_key) { const pal_char_t* prefix = (hive == HKEY_CURRENT_USER) ? _X("HKCU\\") : _X("HKLM\\"); - const pal_char_t value[] = _X("\\InstallLocation"); - - size_t prefix_len = wcslen(prefix); - size_t sub_key_len = wcslen(sub_key); - size_t value_len = ARRAY_SIZE(value) - 1; - size_t total = prefix_len + sub_key_len + value_len + 1; + size_t total = pal_strlen(prefix) + pal_strlen(sub_key) + STRING_LENGTH("\\InstallLocation") + 1; pal_char_t* result = (pal_char_t*)malloc(total * sizeof(pal_char_t)); if (result == NULL) return NULL; - memcpy(result, prefix, prefix_len * sizeof(pal_char_t)); - memcpy(result + prefix_len, sub_key, sub_key_len * sizeof(pal_char_t)); - memcpy(result + prefix_len + sub_key_len, value, value_len * sizeof(pal_char_t)); - result[total - 1] = L'\0'; + pal_str_printf(result, total, _X("%s%s\\InstallLocation"), prefix, sub_key); return result; } @@ -393,7 +354,7 @@ pal_char_t* pal_get_dotnet_self_registered_dir(void) } } - // RegOpenKeyEx is required to force the 32-bit registry view via KEY_WOW64_32KEY. + // Always look in the 32-bit registry view via KEY_WOW64_32KEY. HKEY hkey = NULL; LSTATUS status = RegOpenKeyExW(hive, sub_key, 0, KEY_READ | KEY_WOW64_32KEY, &hkey); if (status != ERROR_SUCCESS) @@ -465,7 +426,7 @@ pal_char_t* pal_get_default_installation_dir(void) const pal_char_t dotnet_seg[] = _X("\\dotnet"); const pal_char_t arch_seg[] = _X("\\") _STRINGIFY(CURRENT_ARCH_NAME); - size_t canonical_len = wcslen(canonical); + size_t canonical_len = pal_strlen(canonical); size_t dotnet_len = ARRAY_SIZE(dotnet_seg) - 1; bool emulating_x64 = pal_is_emulating_x64(); size_t arch_len = emulating_x64 ? (ARRAY_SIZE(arch_seg) - 1) : 0; diff --git a/src/native/corehost/hostmisc/trace.c b/src/native/corehost/hostmisc/trace.c index 3c8e8c3774eb6a..347d04459fb3d6 100644 --- a/src/native/corehost/hostmisc/trace.c +++ b/src/native/corehost/hostmisc/trace.c @@ -72,7 +72,7 @@ static void trace_lock_release(void) } // Reads DOTNET_HOST_, falling back to COREHOST_ for compat. -// Returns a heap-allocated NUL-terminated value, or NULL if neither variable +// Returns a heap-allocated null-terminated value, or NULL if neither variable // is set. Caller must free() the returned pointer. static pal_char_t* get_host_env_var(const pal_char_t* name) { diff --git a/src/native/corehost/hostmisc/utils.c b/src/native/corehost/hostmisc/utils.c index 7c017fcf476a94..09ca8283043bb1 100644 --- a/src/native/corehost/hostmisc/utils.c +++ b/src/native/corehost/hostmisc/utils.c @@ -5,16 +5,17 @@ #include "utils.h" +#include #include -bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len) +void utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len) { if (path == NULL) { if (out_name_len > 0) out_name[0] = _X('\0'); - return true; + return; } const pal_char_t* last_sep = pal_strrchr(path, DIR_SEPARATOR); @@ -22,51 +23,63 @@ bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out size_t len = pal_strlen(name); if (len >= out_name_len) { + assert(false && "utils_get_filename: out_name buffer too small"); if (out_name_len > 0) out_name[0] = _X('\0'); - return false; + return; } memcpy(out_name, name, (len + 1) * sizeof(pal_char_t)); - return true; } -void utils_append_path(pal_char_t* path, size_t path_len, const pal_char_t* component) +void utils_append_path(pal_char_t* path_buffer, size_t path_buffer_len, const pal_char_t* component) { if (component == NULL || component[0] == _X('\0')) return; - size_t current_len = pal_strlen(path); + size_t current_len = pal_strlen(path_buffer); size_t comp_len = pal_strlen(component); - if (current_len == 0) + // Insert a separator only when the buffer is non-empty, doesn't already end in one, + // and the component doesn't already start with one. + bool need_sep = current_len > 0 + && path_buffer[current_len - 1] != DIR_SEPARATOR + && component[0] != DIR_SEPARATOR; + + if (current_len + (need_sep ? 1u : 0u) + comp_len >= path_buffer_len) { - if (comp_len < path_len) - memcpy(path, component, (comp_len + 1) * sizeof(pal_char_t)); + assert(false && "utils_append_path: path_buffer too small"); return; } - bool need_sep = (path[current_len - 1] != DIR_SEPARATOR) && (component[0] != DIR_SEPARATOR); - if (current_len + (need_sep ? 1u : 0u) + comp_len >= path_len) - return; - if (need_sep) - path[current_len++] = DIR_SEPARATOR; + path_buffer[current_len++] = DIR_SEPARATOR; - memcpy(path + current_len, component, (comp_len + 1) * sizeof(pal_char_t)); + memcpy(path_buffer + current_len, component, (comp_len + 1) * sizeof(pal_char_t)); } -pal_char_t* utils_file_exists_in_dir_alloc(const pal_char_t* dir, const pal_char_t* file_name) +pal_char_t* utils_append_path_alloc(const pal_char_t* path, const pal_char_t* component) +{ + size_t cap = pal_strlen(path) + pal_strlen(component) + 2; // +1 separator, +1 null terminator + pal_char_t* out = (pal_char_t*)malloc(cap * sizeof(pal_char_t)); + if (out == NULL) + return NULL; + + out[0] = _X('\0'); + utils_append_path(out, cap, path); + utils_append_path(out, cap, component); + return out; +} + +pal_char_t* utils_find_file_in_dir(const pal_char_t* dir, const pal_char_t* file_name) { if (dir == NULL || file_name == NULL) return NULL; - size_t cap = pal_strlen(dir) + pal_strlen(file_name) + 2; // +1 separator, +1 NUL - pal_char_t* file_path = (pal_char_t*)malloc(cap * sizeof(pal_char_t)); - file_path[0] = _X('\0'); - utils_append_path(file_path, cap, dir); - utils_append_path(file_path, cap, file_name); + pal_char_t* file_path = utils_append_path_alloc(dir, file_name); + if (file_path == NULL) + return NULL; if (!pal_file_exists(file_path)) { @@ -77,69 +90,54 @@ pal_char_t* utils_file_exists_in_dir_alloc(const pal_char_t* dir, const pal_char return file_path; } -pal_char_t* utils_get_directory_alloc(const pal_char_t* path) +pal_char_t* utils_get_directory(const pal_char_t* path) { if (path == NULL) return NULL; - // Drop trailing separators (matches C++ get_directory which pops them off the working copy). - size_t len = pal_strlen(path); - while (len > 0 && path[len - 1] == DIR_SEPARATOR) - len--; + size_t path_len = pal_strlen(path); + pal_char_t* result = (pal_char_t*)malloc((path_len + 2) * sizeof(pal_char_t)); // +2 for trailing separator and null terminator + if (result == NULL) + return NULL; + + memcpy(result, path, (path_len + 1) * sizeof(pal_char_t)); - // Find the last separator within [0, len). - intptr_t sep_pos = -1; - for (intptr_t i = (intptr_t)len - 1; i >= 0; i--) - { - if (path[i] == DIR_SEPARATOR) - { - sep_pos = i; - break; - } - } + // Drop trailing separators. + size_t len = path_len; + while (len > 0 && result[len - 1] == DIR_SEPARATOR) + result[--len] = _X('\0'); - size_t prefix_len; - if (sep_pos < 0) - { - // No separator: result is path[0..len) + DIR_SEPARATOR. - prefix_len = len; - } - else + // Find the last separator + pal_char_t* last_sep = pal_strrchr(result, DIR_SEPARATOR); + if (last_sep != NULL) { - // Skip any run of separators that precedes sep_pos (matches the C++ pos-- loop). - intptr_t pos = sep_pos; - while (pos >= 0 && path[pos] == DIR_SEPARATOR) - pos--; - prefix_len = (size_t)(pos + 1); + // Strip any trailing separators before the last separator. + len = (size_t)(last_sep - result); + while (len > 0 && result[len - 1] == DIR_SEPARATOR) + len--; } - pal_char_t* result = (pal_char_t*)malloc((prefix_len + 2) * sizeof(pal_char_t)); - if (result == NULL) - return NULL; - - if (prefix_len > 0) - memcpy(result, path, prefix_len * sizeof(pal_char_t)); - result[prefix_len] = DIR_SEPARATOR; - result[prefix_len + 1] = _X('\0'); + result[len] = DIR_SEPARATOR; + result[len + 1] = _X('\0'); return result; } pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key) { - pal_char_t* file_path = pal_getenv(env_key); - if (file_path == NULL) + pal_char_t* env_value = pal_getenv(env_key); + if (env_value == NULL) return NULL; - pal_char_t* canonical = pal_fullpath(file_path, /* skip_error_logging */ false); - if (canonical == NULL) + pal_char_t* file_path = pal_fullpath(env_value, /* skip_error_logging */ false); + if (file_path == NULL) { - trace_verbose(_X("Did not find [%s] directory [%s]"), env_key, file_path); - free(file_path); + trace_verbose(_X("Did not find [%s] directory [%s]"), env_key, env_value); + free(env_value); return NULL; } - free(file_path); - return canonical; + free(env_value); + return file_path; } bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root) diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index a7ed5626117226..1a96919d275ab5 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -12,7 +12,7 @@ bool file_exists_in_dir(const pal::string_t& dir, const pal::char_t* file_name, pal::string_t* out_file_path) { - pal_char_t* file_path = utils_file_exists_in_dir_alloc(dir.c_str(), file_name); + pal_char_t* file_path = utils_find_file_in_dir(dir.c_str(), file_name); if (file_path == nullptr) return false; @@ -135,7 +135,7 @@ pal::string_t get_filename(const pal::string_t& path) pal::string_t get_directory(const pal::string_t& path) { - pal_char_t* result = utils_get_directory_alloc(path.c_str()); + pal_char_t* result = utils_get_directory(path.c_str()); if (result == nullptr) return pal::string_t(); diff --git a/src/native/corehost/hostmisc/utils.h b/src/native/corehost/hostmisc/utils.h index c6718d7fc00803..79ca2e68fcd00b 100644 --- a/src/native/corehost/hostmisc/utils.h +++ b/src/native/corehost/hostmisc/utils.h @@ -214,30 +214,25 @@ size_t to_size_t_dbgchecked(T value) extern "C" { #endif -// Write the trailing path component of `path` (the text after the last -// DIR_SEPARATOR) into out_name. Return false if it does not fit. -bool utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len); +void utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len); -// Return the value of a test-only environment variable, or NULL when unset -// or when the product binary is not stamped as a test build. Caller should -// free() the returned pointer. +// Caller should free() the returned pointer. pal_char_t* utils_test_only_getenv(const pal_char_t* name); -// Append `component` to the caller-allocated buffer `path`, inserting a -// DIR_SEPARATOR between them when needed. Silently no-op on overflow or -// when `component` is NULL/empty. -void utils_append_path(pal_char_t* path, size_t path_len, const pal_char_t* component); +void utils_append_path(pal_char_t* path_buffer, size_t path_buffer_len, const pal_char_t* component); + +// Caller should free() the returned pointer. +pal_char_t* utils_append_path_alloc(const pal_char_t* path, const pal_char_t* component); -// Return / if the file exists, otherwise NULL. Caller should -// free() the returned pointer. -pal_char_t* utils_file_exists_in_dir_alloc(const pal_char_t* dir, const pal_char_t* file_name); +// Return / if the file exists, otherwise NULL. +// Caller should free() the returned pointer. +pal_char_t* utils_find_file_in_dir(const pal_char_t* dir, const pal_char_t* file_name); // Return the directory portion of `path`, always ending with DIR_SEPARATOR. // Caller should free() the returned pointer. -pal_char_t* utils_get_directory_alloc(const pal_char_t* path); +pal_char_t* utils_get_directory(const pal_char_t* path); -// Read `env_key` and return the canonicalized value, or NULL if unset or -// canonicalization fails. Caller should free() the returned pointer. +// Caller should free() the returned pointer. pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key); // Find the .NET install root from environment variables in priority order: @@ -247,9 +242,7 @@ pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key); // Caller should free() out_dotnet_root. bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root); -// Return the download URL for `framework_name` at `framework_version`, or -// the runtime URL when `framework_name` is NULL or empty. Caller should free() -// the returned pointer. +// Caller should free() the returned pointer. pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version); #ifdef __cplusplus From 9b32ea32bb16173ef27fec9cfdf18aabf95f3ffc Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 11 Jun 2026 14:01:50 -0700 Subject: [PATCH 07/14] Convert test_only_getenv to C Move the test-only environment variable helper to utils.c as utils_test_only_getenv. The C++ test_only_getenv now wraps the C version, so existing C++ callers (pal.unix.cpp, pal.windows.cpp) are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/hostmisc/utils.c | 20 ++++++++++++++++++ src/native/corehost/hostmisc/utils.cpp | 29 +++++--------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/native/corehost/hostmisc/utils.c b/src/native/corehost/hostmisc/utils.c index 09ca8283043bb1..ae5cf3ec3ed055 100644 --- a/src/native/corehost/hostmisc/utils.c +++ b/src/native/corehost/hostmisc/utils.c @@ -140,6 +140,26 @@ pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key) return file_path; } +#define TEST_ONLY_MARKER "d38cc827-e34f-4453-9df4-1e796e9f1d07" + +// Retrieves environment variable which is only used for testing. +// This will return the value of the variable only if the product binary is stamped +// with test-only marker. +pal_char_t* utils_test_only_getenv(const pal_char_t* name) +{ + // This is a static variable which is embedded in the product binary (somewhere). + // The marker values is a GUID so that it's unique and can be found by doing a simple search on the file + // The first character is used as the decider: + // - Default value is 'd' (stands for disabled) - test only behavior is disabled + // - To enable test-only behaviors set it to 'e' (stands for enabled) + volatile static char embed[sizeof(TEST_ONLY_MARKER)] = TEST_ONLY_MARKER; + + if (embed[0] != 'e') + return NULL; + + return pal_getenv(name); +} + bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root) { if (out_env_var_name == NULL || out_dotnet_root == NULL) diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index 1a96919d275ab5..fdfa3d6436acb9 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -482,37 +482,18 @@ pal::string_t to_upper(const pal::char_t* in) { return ret; } -#define TEST_ONLY_MARKER "d38cc827-e34f-4453-9df4-1e796e9f1d07" - // Retrieves environment variable which is only used for testing. // This will return the value of the variable only if the product binary is stamped // with test-only marker. bool test_only_getenv(const pal::char_t* name, pal::string_t* recv) { - // This is a static variable which is embedded in the product binary (somewhere). - // The marker values is a GUID so that it's unique and can be found by doing a simple search on the file - // The first character is used as the decider: - // - Default value is 'd' (stands for disabled) - test only behavior is disabled - // - To enable test-only behaviors set it to 'e' (stands for enabled) - constexpr size_t EMBED_SIZE = sizeof(TEST_ONLY_MARKER) / sizeof(TEST_ONLY_MARKER[0]); - volatile static char embed[EMBED_SIZE] = TEST_ONLY_MARKER; - - if (embed[0] != 'e') - { + pal_char_t* value = utils_test_only_getenv(name); + if (value == nullptr) return false; - } - return pal::getenv(name, recv); -} - -extern "C" pal_char_t* utils_test_only_getenv(const pal_char_t* name) -{ - pal::string_t value; - if (!test_only_getenv(name, &value)) - return nullptr; - - pal_char_t* dup = pal_strdup(value.c_str()); - return dup; + recv->assign(value); + free(value); + return true; } extern "C" pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version) From 60c6ba34abeefc73ed8c1f08f7f065248f016e33 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 12 Jun 2026 11:22:59 -0700 Subject: [PATCH 08/14] Convert get_download_url and get_runtime_id to C Add utils_get_download_url and utils_get_runtime_id in C, and reduce the C++ versions to thin wrappers around them. fxr_resolver.c now writes into a stack-allocated MAX_DOWNLOAD_URL_LEN buffer instead of allocating with malloc. The C download URL drops runtime OS detection (was pal_get_current_os_rid_platform on Linux); the URL now always uses the build-time FALLBACK_HOST_OS, which is acceptable since it is purely informational and aka.ms accepts either form. Move HOST_RID_PLATFORM out of the C++-only section of pal.h so the C implementations can use it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/fxr_resolver.c | 5 +-- src/native/corehost/hostmisc/pal.h | 36 ++++++++--------- src/native/corehost/hostmisc/utils.c | 37 ++++++++++++++++++ src/native/corehost/hostmisc/utils.cpp | 54 +++++--------------------- src/native/corehost/hostmisc/utils.h | 11 ++++-- 5 files changed, 74 insertions(+), 69 deletions(-) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index 0999c9472860b2..62b435f33de71d 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -129,7 +129,6 @@ static void print_missing_runtime_error( pal_char_t* self_registered_dir = NULL; pal_char_t* registered_config_location = NULL; pal_char_t* default_install_location = NULL; - pal_char_t* download_url = NULL; // Build the message as an array of string fragments, then concatenate once // at the end. Current upper bound is ~24 message_parts; 32 is comfortable. @@ -234,7 +233,8 @@ static void print_missing_runtime_error( } *dst = _X('\0'); - download_url = utils_get_download_url(NULL, NULL); + pal_char_t download_url[MAX_DOWNLOAD_URL_LEN]; + utils_get_download_url(download_url, ARRAY_SIZE(download_url), NULL, NULL); trace_error( MISSING_RUNTIME_ERROR_FORMAT, @@ -251,7 +251,6 @@ static void print_missing_runtime_error( free(self_registered_dir); free(registered_config_location); free(default_install_location); - free(download_url); } bool fxr_resolver_try_get_path( diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 4656bb335de09b..26849a76bdffa1 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -220,6 +220,24 @@ pal_char_t* pal_get_dotnet_self_registered_config_location(void); #define LIBFXR_NAME LIB_FILE_NAME_X("hostfxr") #define LIBHOSTPOLICY_NAME LIB_FILE_NAME_X("hostpolicy") +// When running on a platform that is not supported in RID fallback graph (because it was unknown +// at the time the SharedFX in question was built), we need to use a reasonable fallback RID to allow +// consuming the native assets. +// +// For Windows and OSX, we will maintain the last highest RID-Platform we are known to support for them as the +// degree of compat across their respective releases is usually high. +// +// We cannot maintain the same (compat) invariant for linux and thus, we will fallback to using lowest RID-Platform. +#if defined(TARGET_WINDOWS) + #define HOST_RID_PLATFORM "win" +#elif defined(TARGET_OSX) + #define HOST_RID_PLATFORM "osx" +#elif defined(TARGET_ANDROID) + #define HOST_RID_PLATFORM "linux-bionic" +#else + #define HOST_RID_PLATFORM FALLBACK_HOST_OS +#endif + // ============================================================================ // C++ section (the original pal:: namespace surface) // ============================================================================ @@ -258,28 +276,10 @@ pal_char_t* pal_get_dotnet_self_registered_config_location(void); #endif -// When running on a platform that is not supported in RID fallback graph (because it was unknown -// at the time the SharedFX in question was built), we need to use a reasonable fallback RID to allow -// consuming the native assets. -// -// For Windows and OSX, we will maintain the last highest RID-Platform we are known to support for them as the -// degree of compat across their respective releases is usually high. -// -// We cannot maintain the same (compat) invariant for linux and thus, we will fallback to using lowest RID-Platform. #if !defined(PATH_MAX) && !defined(_WIN32) #define PATH_MAX 4096 #endif -#if defined(TARGET_WINDOWS) - #define HOST_RID_PLATFORM "win" -#elif defined(TARGET_OSX) - #define HOST_RID_PLATFORM "osx" -#elif defined(TARGET_ANDROID) - #define HOST_RID_PLATFORM "linux-bionic" -#else - #define HOST_RID_PLATFORM FALLBACK_HOST_OS -#endif - namespace pal { #if defined(_WIN32) diff --git a/src/native/corehost/hostmisc/utils.c b/src/native/corehost/hostmisc/utils.c index ae5cf3ec3ed055..5260fca2df5a96 100644 --- a/src/native/corehost/hostmisc/utils.c +++ b/src/native/corehost/hostmisc/utils.c @@ -200,3 +200,40 @@ bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_cha return false; } + +pal_char_t* utils_get_runtime_id(void) +{ + pal_char_t* env_rid = pal_getenv(_X("DOTNET_RUNTIME_ID")); + if (env_rid != NULL) + return env_rid; + + return pal_strdup(_STRINGIFY(HOST_RID_PLATFORM) _X("-") _STRINGIFY(CURRENT_ARCH_NAME)); +} + +void utils_get_download_url(pal_char_t* out_url, size_t out_url_len, const pal_char_t* framework_name, const pal_char_t* framework_version) +{ + pal_char_t* rid = utils_get_runtime_id(); + + pal_char_t query[MAX_DOWNLOAD_URL_LEN / 2]; + if (framework_name != NULL) + { + if (framework_version != NULL && framework_version[0] != _X('\0')) + { + pal_str_printf(query, ARRAY_SIZE(query), _X("framework=%s&framework_version=%s"), framework_name, framework_version); + } + else + { + pal_str_printf(query, ARRAY_SIZE(query), _X("framework=%s"), framework_name); + } + } + else + { + pal_str_printf(query, ARRAY_SIZE(query), _X("missing_runtime=true")); + } + + pal_str_printf(out_url, out_url_len, + DOTNET_CORE_APPLAUNCH_URL _X("?%s&arch=") _STRINGIFY(CURRENT_ARCH_NAME) _X("&rid=%s&os=") _STRINGIFY(FALLBACK_HOST_OS), + query, rid); + + free(rid); +} diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index fdfa3d6436acb9..4c8f18f77f9698 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -238,11 +238,10 @@ const pal::char_t* get_current_arch_name() pal::string_t get_runtime_id() { - pal::string_t rid; - if (try_get_runtime_id_from_env(rid)) - return rid; - - return _STRINGIFY(HOST_RID_PLATFORM) _X("-") _STRINGIFY(CURRENT_ARCH_NAME); + pal_char_t* rid = utils_get_runtime_id(); + pal::string_t result = rid; + free(rid); + return result; } bool try_get_runtime_id_from_env(pal::string_t& out_rid) @@ -309,12 +308,12 @@ void get_framework_locations(const pal::string_t& dotnet_dir, const bool disable bool get_file_path_from_env(const pal::char_t* env_key, pal::string_t* recv) { recv->clear(); - pal_char_t* canonical = utils_get_file_path_from_env(env_key); - if (canonical == nullptr) + pal_char_t* file_path = utils_get_file_path_from_env(env_key); + if (file_path == nullptr) return false; - recv->assign(canonical); - free(canonical); + recv->assign(file_path); + free(file_path); return true; } @@ -416,35 +415,8 @@ pal::string_t get_dotnet_root_from_fxr_path(const pal::string_t& fxr_path) pal::string_t get_download_url(const pal::char_t* framework_name, const pal::char_t* framework_version) { - pal::string_t url = DOTNET_CORE_APPLAUNCH_URL _X("?"); - if (framework_name != nullptr && pal::strlen(framework_name) > 0) - { - url.append(_X("framework=")); - url.append(framework_name); - if (framework_version != nullptr && pal::strlen(framework_version) > 0) - { - url.append(_X("&framework_version=")); - url.append(framework_version); - } - } - else - { - url.append(_X("missing_runtime=true")); - } - - const pal::char_t* arch = get_current_arch_name(); - url.append(_X("&arch=")); - url.append(arch); - url.append(_X("&rid=")); - url.append(get_runtime_id()); - - pal::string_t os = pal::get_current_os_rid_platform(); - if (os.empty()) - os = pal::get_current_os_fallback_rid(); - - url.append(_X("&os=")); - url.append(os); - + pal_char_t url[MAX_DOWNLOAD_URL_LEN]; + utils_get_download_url(url, ARRAY_SIZE(url), framework_name, framework_version); return url; } @@ -495,9 +467,3 @@ bool test_only_getenv(const pal::char_t* name, pal::string_t* recv) free(value); return true; } - -extern "C" pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version) -{ - pal::string_t url = get_download_url(framework_name, framework_version); - return pal_strdup(url.c_str()); -} diff --git a/src/native/corehost/hostmisc/utils.h b/src/native/corehost/hostmisc/utils.h index 79ca2e68fcd00b..e9952cf8e22901 100644 --- a/src/native/corehost/hostmisc/utils.h +++ b/src/native/corehost/hostmisc/utils.h @@ -216,9 +216,6 @@ extern "C" { void utils_get_filename(const pal_char_t* path, pal_char_t* out_name, size_t out_name_len); -// Caller should free() the returned pointer. -pal_char_t* utils_test_only_getenv(const pal_char_t* name); - void utils_append_path(pal_char_t* path_buffer, size_t path_buffer_len, const pal_char_t* component); // Caller should free() the returned pointer. @@ -235,6 +232,9 @@ pal_char_t* utils_get_directory(const pal_char_t* path); // Caller should free() the returned pointer. pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key); +// Caller should free() the returned pointer. +pal_char_t* utils_test_only_getenv(const pal_char_t* name); + // Find the .NET install root from environment variables in priority order: // - DOTNET_ROOT_ // - If running Windows WOW64 only, DOTNET_ROOT(x86) @@ -243,7 +243,10 @@ pal_char_t* utils_get_file_path_from_env(const pal_char_t* env_key); bool utils_get_dotnet_root_from_env(const pal_char_t** out_env_var_name, pal_char_t** out_dotnet_root); // Caller should free() the returned pointer. -pal_char_t* utils_get_download_url(const pal_char_t* framework_name, const pal_char_t* framework_version); +pal_char_t* utils_get_runtime_id(void); + +#define MAX_DOWNLOAD_URL_LEN 512 +void utils_get_download_url(pal_char_t* out_url, size_t out_url_len, const pal_char_t* framework_name, const pal_char_t* framework_version); #ifdef __cplusplus } From 212ace7d4fd64829af7854a903ec4708f8565cba Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 12 Jun 2026 13:20:58 -0700 Subject: [PATCH 09/14] Normalize long paths in pal_readdir_onlydirectories The C++ pal::readdir_onlydirectories normalized the directory path via LongFile::ShouldNormalize + pal::fullpath before enumerating. When the single-arg overload was re-pointed at the new C pal_readdir_onlydirectories, that normalization was lost, so enumerating version-numbered folders under a long (>= MAX_PATH) or not-fully-qualified fxr root would fail where it previously succeeded. Replicate the ShouldNormalize gate in C (is_path_not_fully_qualified / should_normalize_path) and canonicalize via pal_fullpath before building the FindFirstFileExW search string. Also add named defines for the long-path prefixes and path separators and reuse them across is_path_normalized, pal_fullpath, and is_path_not_fully_qualified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/hostmisc/pal.windows.c | 90 +++++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/src/native/corehost/hostmisc/pal.windows.c b/src/native/corehost/hostmisc/pal.windows.c index 88cab942bc36ac..e725c6bb28b16a 100644 --- a/src/native/corehost/hostmisc/pal.windows.c +++ b/src/native/corehost/hostmisc/pal.windows.c @@ -13,6 +13,16 @@ #include +// Long-path extended-syntax prefixes for Windows paths. +#define EXTENDED_PATH_PREFIX _X("\\\\?\\") // "\\?\" (also covers "\\?\UNC\") +#define UNC_EXTENDED_PATH_PREFIX _X("\\\\?\\UNC\\") // "\\?\UNC\" +#define DEVICE_PATH_PREFIX _X("\\\\.\\") // "\\.\" + +// Windows path separators. +#define DIR_SEPARATOR L'\\' +#define ALT_DIR_SEPARATOR L'/' +#define VOLUME_SEPARATOR L':' + pal_char_t* pal_get_own_executable_path(void) { // GetModuleFileNameW returns 0 on failure, the number of characters @@ -92,16 +102,13 @@ pal_char_t* pal_getenv(const pal_char_t* name) // extended-syntax prefixes: // \\?\ (extended path prefix - includes \\?\UNC\) // \\.\ (device path prefix) -// Mirrors LongFile::IsNormalized. static bool is_path_normalized(const pal_char_t* path) { if (path[0] == L'\0') return true; - return path[0] == L'\\' - && path[1] == L'\\' - && (path[2] == L'?' || path[2] == L'.') - && path[3] == L'\\'; + return wcsncmp(path, EXTENDED_PATH_PREFIX, STRING_LENGTH(EXTENDED_PATH_PREFIX)) == 0 + || wcsncmp(path, DEVICE_PATH_PREFIX, STRING_LENGTH(DEVICE_PATH_PREFIX)) == 0; } pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) @@ -135,8 +142,8 @@ pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) if (size >= MAX_PATH) { // Need a larger buffer. Allocate enough room for the canonicalized - // path plus the longest long-path prefix ("\\?\UNC\" = 8 chars). - const DWORD prefix_headroom = 8; + // path plus the longest long-path prefix ("\\?\UNC\"). + const DWORD prefix_headroom = STRING_LENGTH(UNC_EXTENDED_PATH_PREFIX); pal_char_t* new_buf = (pal_char_t*)realloc(buf, (size + prefix_headroom) * sizeof(pal_char_t)); if (new_buf == NULL) { @@ -158,9 +165,9 @@ pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging) // For UNC paths (\\server\share\...), strip the leading "\\" and // prepend "\\?\UNC\"; otherwise just prepend "\\?\". bool is_unc = (buf[0] == L'\\' && buf[1] == L'\\'); - const pal_char_t* prefix = is_unc ? L"\\\\?\\UNC\\" : L"\\\\?\\"; - DWORD prefix_len = is_unc ? 8 : 4; - DWORD skip = is_unc ? 2 : 0; + const pal_char_t* prefix = is_unc ? UNC_EXTENDED_PATH_PREFIX : EXTENDED_PATH_PREFIX; + DWORD prefix_len = is_unc ? STRING_LENGTH(UNC_EXTENDED_PATH_PREFIX) : STRING_LENGTH(EXTENDED_PATH_PREFIX); + DWORD skip = is_unc ? STRING_LENGTH(_X("\\\\")) : 0; // drop the UNC's leading "\\" // Make room for the prefix by shifting the path right (including the NUL). memmove(buf + prefix_len, buf + skip, (new_size - skip + 1) * sizeof(pal_char_t)); @@ -187,24 +194,83 @@ bool pal_file_exists(const pal_char_t* path) return exists; } +static bool is_dir_separator(pal_char_t c) +{ + return c == DIR_SEPARATOR || c == ALT_DIR_SEPARATOR; +} + +// Returns true if the path is relative to the current drive or working +// directory (i.e. not rooted at a specific drive or UNC share), and therefore +// must be canonicalized before it can be reliably used. +static bool is_path_not_fully_qualified(const pal_char_t* path) +{ + size_t len = pal_strlen(path); + + // Too short to encode a drive ("X:") or UNC ("\\") root. + if (len < 2) + return true; + + // Starts with a separator: fully qualified only if it's a UNC path, + // i.e. the second character is also a separator ("\\server\share"). + if (is_dir_separator(path[0])) + return !is_dir_separator(path[1]); + + // Otherwise it must be a drive-rooted path of the form "X:\": at least + // three characters, a volume separator at index 1, and a directory + // separator at index 2. + return len < 3 + || path[1] != VOLUME_SEPARATOR + || !is_dir_separator(path[2]); +} + +// Returns true if the path needs normalization (canonicalization, and the \\?\ +// prefix for long paths): it isn't already normalized and is either not fully +// qualified or at least MAX_PATH characters long. +static bool should_normalize_path(const pal_char_t* path) +{ + if (is_path_normalized(path)) + return false; + + if (!is_path_not_fully_qualified(path) && pal_strlen(path) < MAX_PATH) + return false; + + return true; +} + bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx) { if (path == NULL || callback == NULL) return false; + // Long or not-fully-qualified paths must be canonicalized (and prefixed with + // \\?\ when long) before being passed to FindFirstFileExW. + pal_char_t* normalized = NULL; + if (should_normalize_path(path)) + { + normalized = pal_fullpath(path, /*skip_error_logging*/ false); + if (normalized == NULL) + return false; + + path = normalized; + } + // Build the search string: path + "\\*". One extra char beyond path // length is needed for the separator if path doesn't already end with one. size_t path_len = pal_strlen(path); size_t search_len = path_len + 3; // worst case: '\\', '*', NUL pal_char_t* search = (pal_char_t*)malloc(search_len * sizeof(pal_char_t)); if (search == NULL) + { + free(normalized); return false; + } memcpy(search, path, path_len * sizeof(pal_char_t)); + free(normalized); // path has been copied into search; no longer needed size_t pos = path_len; - if (pos == 0 || (search[pos - 1] != L'\\' && search[pos - 1] != L'/')) + if (pos == 0 || !is_dir_separator(search[pos - 1])) { - search[pos++] = L'\\'; + search[pos++] = DIR_SEPARATOR; } search[pos++] = L'*'; search[pos] = L'\0'; From cd87af5760163c7f666895ded0394e562548cd73 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 15 Jun 2026 10:59:21 -0700 Subject: [PATCH 10/14] Define CLR_CMAKE_TARGET_ARCH_UPPER for static singlefilehost build The singlefilehost is built from the coreclr tree (src/coreclr/CMakeLists.txt), which runs apphost/static/CMakeLists.txt's own configure_file for configure.h.in. CLR_CMAKE_TARGET_ARCH_UPPER was only set in corehost/CMakeLists.txt, so it was empty in this scope, making CURRENT_ARCH_NAME_UPPER empty and degrading DOTNET_ROOT_ to DOTNET_ROOT_ for single-file apps. Compute the uppercase arch in the static path before configuring the header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/apphost/static/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/native/corehost/apphost/static/CMakeLists.txt b/src/native/corehost/apphost/static/CMakeLists.txt index 0addec43ccd6eb..17a334d1302531 100644 --- a/src/native/corehost/apphost/static/CMakeLists.txt +++ b/src/native/corehost/apphost/static/CMakeLists.txt @@ -14,6 +14,8 @@ include_directories(${CLR_ARTIFACTS_OBJ_DIR}) # Generated version files add_subdirectory(../../hostmisc hostmisc) +string(TOUPPER "${CLR_CMAKE_TARGET_ARCH}" CLR_CMAKE_TARGET_ARCH_UPPER) + configure_file(${CLR_SRC_NATIVE_DIR}/corehost/configure.h.in ${GENERATED_INCLUDE_DIR}/corehost/configure.h) target_include_directories(hostmisc_interface INTERFACE ${GENERATED_INCLUDE_DIR}/corehost) From 95e3e22525412346dd424fe213c01058f6cc4c13 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 16 Jun 2026 15:08:46 -0700 Subject: [PATCH 11/14] Address PR review: harden OOM diagnostic path, drop redundant DIR_SEPARATOR Guard NULL registered_config_location and the message-buffer malloc in print_missing_runtime_error so the missing-runtime diagnostic degrades gracefully on OOM instead of dereferencing NULL. Remove the redundant DIR_SEPARATOR redefinition in pal.windows.c and use the one from pal.h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/fxr_resolver.c | 19 +++++++++++-------- src/native/corehost/hostmisc/pal.windows.c | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index 62b435f33de71d..54338fbff7ac4b 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -199,7 +199,7 @@ static void print_missing_runtime_error( bool self_registered_empty = self_registered_dir == NULL || self_registered_dir[0] == _X('\0'); message_parts[index++] = _X("\n Registered location:\n "); - message_parts[index++] = registered_config_location; + message_parts[index++] = registered_config_location != NULL ? registered_config_location : _X(""); message_parts[index++] = _X(" = "); message_parts[index++] = self_registered_empty ? _X("") : self_registered_dir; @@ -224,14 +224,17 @@ static void print_missing_runtime_error( } pal_char_t* location = (pal_char_t*)malloc((total + 1) * sizeof(pal_char_t)); - pal_char_t* dst = location; - for (size_t i = 0; i < index; ++i) + if (location != NULL) { - size_t len = pal_strlen(message_parts[i]); - memcpy(dst, message_parts[i], len * sizeof(pal_char_t)); - dst += len; + pal_char_t* dst = location; + for (size_t i = 0; i < index; ++i) + { + size_t len = pal_strlen(message_parts[i]); + memcpy(dst, message_parts[i], len * sizeof(pal_char_t)); + dst += len; + } + *dst = _X('\0'); } - *dst = _X('\0'); pal_char_t download_url[MAX_DOWNLOAD_URL_LEN]; utils_get_download_url(download_url, ARRAY_SIZE(download_url), NULL, NULL); @@ -242,7 +245,7 @@ static void print_missing_runtime_error( host_path != NULL ? host_path : _X(""), _STRINGIFY(CURRENT_ARCH_NAME), _STRINGIFY(HOST_VERSION), - location, + location != NULL ? location : _X(""), download_url, _STRINGIFY(HOST_VERSION)); diff --git a/src/native/corehost/hostmisc/pal.windows.c b/src/native/corehost/hostmisc/pal.windows.c index e725c6bb28b16a..95cf4ebe04f2d7 100644 --- a/src/native/corehost/hostmisc/pal.windows.c +++ b/src/native/corehost/hostmisc/pal.windows.c @@ -19,7 +19,6 @@ #define DEVICE_PATH_PREFIX _X("\\\\.\\") // "\\.\" // Windows path separators. -#define DIR_SEPARATOR L'\\' #define ALT_DIR_SEPARATOR L'/' #define VOLUME_SEPARATOR L':' From 26e9342cac9339de854b19d8a0bb8988a1f42727 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 17 Jun 2026 14:03:04 -0700 Subject: [PATCH 12/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/native/corehost/hostmisc/utils.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index 4c8f18f77f9698..5dfd6b8af8a28e 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -239,6 +239,9 @@ const pal::char_t* get_current_arch_name() pal::string_t get_runtime_id() { pal_char_t* rid = utils_get_runtime_id(); + if (rid == nullptr) + return pal::string_t(_STRINGIFY(HOST_RID_PLATFORM) _X("-") _STRINGIFY(CURRENT_ARCH_NAME)); + pal::string_t result = rid; free(rid); return result; From 7d767b76894dcf15cfa8626c63241f8fdc916b0a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 17 Jun 2026 14:22:20 -0700 Subject: [PATCH 13/14] Update src/native/corehost/fxr_resolver.c --- src/native/corehost/fxr_resolver.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index 54338fbff7ac4b..be658015c1ee44 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -70,6 +70,8 @@ static bool get_latest_fxr(const pal_char_t* fxr_root, pal_char_t** out_fxr_path return false; } + // SemVer does not define a size limit on version string, but calls out a reasonable max of 255 characters: + // https://semver.org/#does-semver-have-a-size-limit-on-the-version-string pal_char_t max_ver_str[256]; c_fx_ver_as_str(&ctx.max_ver, max_ver_str, ARRAY_SIZE(max_ver_str)); c_fx_ver_cleanup(&ctx.max_ver); From 04963b2b4fccb99f93bad571eb0dfcb4618302d0 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 17 Jun 2026 17:13:05 -0700 Subject: [PATCH 14/14] Address PR review: fix empty framework_name, harden OOM rid path, assert dotnet_root, merge wow64/emulation into pal_get_process_emulation enum Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/corehost/fxr_resolver.c | 2 + src/native/corehost/hostmisc/pal.h | 23 ++++++--- src/native/corehost/hostmisc/pal.unix.c | 18 +++---- src/native/corehost/hostmisc/pal.unix.cpp | 4 +- src/native/corehost/hostmisc/pal.windows.c | 51 ++++++++++---------- src/native/corehost/hostmisc/pal.windows.cpp | 4 +- src/native/corehost/hostmisc/utils.c | 7 +-- 7 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/native/corehost/fxr_resolver.c b/src/native/corehost/fxr_resolver.c index be658015c1ee44..536ac39cbd3cac 100644 --- a/src/native/corehost/fxr_resolver.c +++ b/src/native/corehost/fxr_resolver.c @@ -426,6 +426,8 @@ bool fxr_resolver_try_get_path_from_dotnet_root( { *out_fxr_path = NULL; + assert(dotnet_root != NULL); + pal_char_t* fxr_dir = get_fxr_dir(dotnet_root); if (fxr_dir == NULL) return false; diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 26849a76bdffa1..b01372953ef84c 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -160,9 +160,21 @@ static inline pal_char_t* pal_strndup(const pal_char_t* src, size_t len) // trace messages are suppressed (used when probing for optional files). pal_char_t* pal_fullpath(const pal_char_t* path, bool skip_error_logging); -// Returns true if the current process is a 32-bit process running on a -// 64-bit Windows OS. Returns false on non-Windows. -bool pal_is_running_in_wow64(void); +// Describes whether the current process is running under an OS compatibility +// layer rather than natively on the host architecture. +typedef enum +{ + pal_process_emulation_none = 0, + // 32-bit (x86) process running on a 64-bit Windows OS (WOW64) + pal_process_emulation_wow64, + // x64 process running under emulation on a non-x64 host: Windows + // x64-on-arm64, or macOS x64-on-arm64 via Rosetta. + pal_process_emulation_x64, +} pal_process_emulation_t; + +// Returns how the OS is emulating the current process, or +// pal_process_emulation_none when it runs natively. +pal_process_emulation_t pal_get_process_emulation(void); // Callback for pal_readdir_onlydirectories. Receives each directory entry // name (just the leaf name, not a full path) and the caller-supplied context. @@ -174,11 +186,6 @@ typedef bool (*pal_readdir_callback_t)(const pal_char_t* entry_name, void* ctx); // stop; returns false if the directory could not be opened. bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t callback, void* ctx); -// Returns true if the current process is an x64 process running under -// emulation on a non-x64 host (Windows x64-on-arm64 via WOW64, or macOS -// x64-on-arm64 via Rosetta). Returns false otherwise. -bool pal_is_emulating_x64(void); - // Returns the directory containing the globally-registered .NET install for // the current architecture, or NULL if no such registration exists. Caller // should free() the returned pointer. diff --git a/src/native/corehost/hostmisc/pal.unix.c b/src/native/corehost/hostmisc/pal.unix.c index 86ecbb3cc40c67..f4ccc0d324fc91 100644 --- a/src/native/corehost/hostmisc/pal.unix.c +++ b/src/native/corehost/hostmisc/pal.unix.c @@ -134,15 +134,10 @@ bool pal_readdir_onlydirectories(const pal_char_t* path, pal_readdir_callback_t return true; } -bool pal_is_running_in_wow64(void) +pal_process_emulation_t pal_get_process_emulation(void) { - return false; -} - -bool pal_is_emulating_x64(void) -{ - int is_translated_process = 0; #if defined(TARGET_OSX) + int is_translated_process = 0; size_t size = sizeof(is_translated_process); if (sysctlbyname("sysctl.proc_translated", &is_translated_process, &size, NULL, 0) == -1) { @@ -152,11 +147,14 @@ bool pal_is_emulating_x64(void) trace_info(_X("Call to sysctlbyname failed: %s"), strerror(errno)); } - return false; + return pal_process_emulation_none; } + + if (is_translated_process == 1) + return pal_process_emulation_x64; #endif - return is_translated_process == 1; + return pal_process_emulation_none; } // Reads up to the first newline from `file`, returning the line (with the @@ -276,7 +274,7 @@ pal_char_t* pal_get_default_installation_dir(void) #if defined(TARGET_OSX) const pal_char_t* base = _X("/usr/local/share/dotnet"); - if (pal_is_emulating_x64()) + if (pal_get_process_emulation() == pal_process_emulation_x64) { return utils_append_path_alloc(base, _STRINGIFY(CURRENT_ARCH_NAME)); } diff --git a/src/native/corehost/hostmisc/pal.unix.cpp b/src/native/corehost/hostmisc/pal.unix.cpp index 1100106150fe79..26d984db325ed5 100644 --- a/src/native/corehost/hostmisc/pal.unix.cpp +++ b/src/native/corehost/hostmisc/pal.unix.cpp @@ -1155,12 +1155,12 @@ void pal::readdir_onlydirectories(const pal::string_t& path, std::vector