From 589c996db61ba9e3577e7a2c5caec823c9871387 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Tue, 4 Aug 2026 21:37:12 +0000 Subject: [PATCH 01/11] Support plugin cuda ep --- sdk_v2/cpp/CMakeLists.txt | 14 + .../src/ep_detection/cuda_ep_bootstrapper.cc | 315 +++++---- .../src/ep_detection/cuda_ep_bootstrapper.h | 26 +- .../src/ep_detection/ep_bundle_installer.cc | 600 +++++++++++++++++ .../src/ep_detection/ep_bundle_installer.h | 84 +++ .../cpp/src/ep_detection/ep_bundle_manifest.h | 52 ++ .../cpp/src/ep_detection/nvml_gpu_detector.cc | 189 ++++++ .../cpp/src/ep_detection/nvml_gpu_detector.h | 19 + .../ep_detection/webgpu_ep_bootstrapper.cc | 337 +++------- .../src/ep_detection/webgpu_ep_bootstrapper.h | 16 +- sdk_v2/cpp/src/http/http_download.cc | 109 ++- sdk_v2/cpp/src/http/http_download.h | 9 +- sdk_v2/cpp/src/manager.cc | 31 +- sdk_v2/cpp/src/manager.h | 2 +- sdk_v2/cpp/src/util/zip_extract.cc | 504 +++++++------- sdk_v2/cpp/src/util/zip_extract.h | 35 +- sdk_v2/cpp/test/CMakeLists.txt | 4 + sdk_v2/cpp/test/internal_api/c_api_test.cc | 17 + .../internal_api/cuda_ep_bootstrapper_test.cc | 18 + .../internal_api/ep_bundle_installer_test.cc | 626 ++++++++++++++++++ .../internal_api/nvml_gpu_detector_test.cc | 58 ++ .../webgpu_ep_bootstrapper_test.cc | 119 ++++ .../cpp/test/internal_api/zip_extract_test.cc | 320 ++++++++- sdk_v2/cpp/test/utils/zip_builder.h | 170 +++++ sdk_v2/cpp/vcpkg.json | 5 + 25 files changed, 2984 insertions(+), 695 deletions(-) create mode 100644 sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc create mode 100644 sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h create mode 100644 sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h create mode 100644 sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc create mode 100644 sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h create mode 100644 sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc create mode 100644 sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc create mode 100644 sdk_v2/cpp/test/internal_api/nvml_gpu_detector_test.cc create mode 100644 sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc create mode 100644 sdk_v2/cpp/test/utils/zip_builder.h diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 3dc080875..22954f2a7 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -65,6 +65,11 @@ find_package(nlohmann_json CONFIG REQUIRED) find_package(azure-storage-blobs-cpp CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) find_package(Microsoft.GSL CONFIG REQUIRED) +# Used by util/zip_extract.cc for in-process, bounded EP archive extraction (no shelling out +# to tar/unzip). Already a transitive dependency of azure-core-cpp; declared directly since we +# now link against it explicitly. +find_package(LibArchive REQUIRED) +find_package(ZLIB REQUIRED) if(FOUNDRY_LOCAL_BUILD_SERVICE) find_package(oatpp CONFIG REQUIRED) @@ -161,8 +166,10 @@ set(FOUNDRY_LOCAL_SOURCES src/download/inference_model_writer.cc src/download/model_registry_client.cc src/ep_detection/cuda_ep_bootstrapper.cc + src/ep_detection/ep_bundle_installer.cc src/ep_detection/ep_detector.cc src/ep_detection/ep_utils.cc + src/ep_detection/nvml_gpu_detector.cc src/ep_detection/runtime_version_info.cc src/ep_detection/webgpu_ep_bootstrapper.cc src/exception.cc @@ -239,8 +246,15 @@ function(foundry_local_configure_target TARGET LINK_SCOPE) Azure::azure-core Azure::azure-storage-blobs spdlog::spdlog + LibArchive::LibArchive + ZLIB::ZLIB + ${CMAKE_DL_LIBS} ) + if(WIN32) + target_link_libraries(${TARGET} ${LINK_SCOPE} shell32 ole32) + endif() + if(TARGET OnnxRuntimeGenAI::OnnxRuntimeGenAI) target_link_libraries(${TARGET} ${LINK_SCOPE} OnnxRuntimeGenAI::OnnxRuntimeGenAI diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 7857655c0..679683731 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -2,53 +2,168 @@ // Licensed under the MIT License. #include "ep_detection/cuda_ep_bootstrapper.h" -#include "ep_detection/ep_utils.h" +#include "ep_detection/nvml_gpu_detector.h" #include "logger.h" -#include "util/file_lock.h" #include "utils.h" -#include "http/http_download.h" -#include "util/zip_extract.h" #include -#include -#include -#include -#include #include +#include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#elif defined(__linux__) && !defined(__ANDROID__) +#include +#endif + namespace { -constexpr const char* kPackageFileName = "cuda-ep.zip"; constexpr const char* kLockFileName = "cuda-ep.lock"; -constexpr const char* kUserAgent = "FoundryLocal"; constexpr int kMaxInstallAttempts = 5; +constexpr const char* kRegistrationName = "CUDAExecutionProvider"; +constexpr const char* kCudaProviderOverrideEnv = "FOUNDRY_LOCAL_CUDA_EP_LIBRARY"; +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +constexpr const char* kGenAiCudaLibrary = "libonnxruntime-genai-cuda.so"; +#endif + +#if defined(_WIN32) && defined(_M_ARM64) +constexpr const char* kCudaBundleId = "cuda-ep-win-arm64-unconfigured"; +#elif defined(_WIN32) && defined(_M_X64) +constexpr const char* kCudaBundleId = "cuda-ep-win-x64-unconfigured"; +#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +constexpr const char* kCudaBundleId = "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.1-20260804-074520"; +constexpr const char* kCudaDownloadUrl = + "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/cuda-ep-linux-x64-20260804-074520.zip"; +constexpr const char* kCudaArchiveSha256 = + "97FF54C93A8E4D6622905AD19BCC9D6B5AA03E54B6B38682FC51117086DCF1F6"; +constexpr uint64_t kCudaArchiveMaxBytes = 512ULL * 1024 * 1024; +#endif + +#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) +fl::EpBundleArtifact DisabledArchiveArtifact(std::string id) { + return fl::EpBundleArtifact{.id = std::move(id), + .url = "", + .is_archive = true, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0}; +} +#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +fl::EpBundleArtifact LinuxCudaArchiveArtifact() { + return fl::EpBundleArtifact{ + .id = "cuda-ep", + .url = kCudaDownloadUrl, + .is_archive = true, + .archive_sha256 = kCudaArchiveSha256, + .extracted_files = + { + {.relative_path = "libonnxruntime-genai-cuda.so", + .sha256 = "86AED826BC9221ABA24A1B9C856A403FD8AEC082B06E6924F616E08A49C6C2F0"}, + {.relative_path = "libonnxruntime_providers_cuda.so", + .sha256 = "9418788F29E45F70904DBA8FA21BE7317C92A45D505B1E50322F3B71A94E52F7"}, + {.relative_path = "version.json", + .sha256 = "65133BC2003C363B4D2C6CB85BC913AFD5291B1D6E2869C3656D940DBF72A505"}, + }, + .archive_max_bytes = kCudaArchiveMaxBytes, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }; +} +#endif -// CUDA EP package is built against the ONNX Runtime version we link against. -constexpr const char* kDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/cuda-ep-20260501-062935.zip"; +std::optional BuildCudaManifest() { +#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) + fl::EpBundleManifest manifest; + manifest.bundle_id = kCudaBundleId; + manifest.provider_relative_path = "onnxruntime_providers_cuda.dll"; + manifest.artifacts = {DisabledArchiveArtifact("cuda-toolkit"), DisabledArchiveArtifact("cudnn"), + DisabledArchiveArtifact("cuda-ep")}; + return manifest; +#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + fl::EpBundleManifest manifest; + manifest.bundle_id = kCudaBundleId; + manifest.provider_relative_path = "libonnxruntime_providers_cuda.so"; + manifest.artifacts = {LinuxCudaArchiveArtifact()}; + return manifest; +#else + return std::nullopt; +#endif +} -struct ExpectedBinary { - const char* filename; - const char* sha256; -}; +#ifdef _WIN32 +bool IsCoreRuntimeLibrary(const std::filesystem::path& filename) { + return _wcsicmp(filename.c_str(), L"onnxruntime.dll") == 0 || + _wcsicmp(filename.c_str(), L"onnxruntime-genai.dll") == 0; +} -constexpr ExpectedBinary kExpectedBinaries[] = { - {"onnxruntime_providers_cuda.dll", "DD540FCFECFBC68B4675C9ADF09C2858CF6B054563859D79598AA2524406A76F"}, - {"onnxruntime-genai-cuda.dll", "BC953F8E2AAFC6219B2D723B65AB8F1A9426A6B7724D6A01ED756FAE8C3DE6AE"}, -}; +bool LoadBundleDependencies(const std::filesystem::path& bin_dir, + const fl::EpBundleManifest& manifest, + fl::ILogger& logger) { + for (const auto& artifact : manifest.artifacts) { + for (const auto& file : artifact.extracted_files) { + const auto path = bin_dir / file.relative_path; + if (_wcsicmp(path.extension().c_str(), L".dll") != 0 || + _wcsicmp(path.filename().c_str(), + std::filesystem::path(manifest.provider_relative_path).filename().c_str()) == 0 || + IsCoreRuntimeLibrary(path.filename())) { + continue; + } -constexpr const char* kRegistrationName = "Foundry.CUDA"; -constexpr const char* kCudaProviderDll = "onnxruntime_providers_cuda.dll"; -constexpr const char* kCudaProviderOverrideEnv = "FOUNDRY_LOCAL_CUDA_EP_LIBRARY"; + if (!LoadLibraryExW(path.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)) { + logger.Log(fl::LogLevel::Warning, + fmt::format("CUDA EP: failed to load dependency '{}' ({})", + path.string(), GetLastError())); + return false; + } + } + } + + return true; +} +#endif + +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +bool LoadGenAiCudaLibrary(const std::filesystem::path& path, void*& handle, fl::ILogger& logger) { + if (handle) { + return true; + } + + dlerror(); + handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL | RTLD_NODELETE); + if (!handle) { + const char* error = dlerror(); + logger.Log(fl::LogLevel::Warning, + fmt::format("CUDA EP: failed to load '{}' ({})", path.string(), error ? error : "unknown error")); + return false; + } + + return true; +} +#endif } // anonymous namespace namespace fl { -CudaEpBootstrapper::CudaEpBootstrapper(std::string ep_dir, EpRegistrationCallback register_ep) - : ep_dir_(std::move(ep_dir)), register_ep_(std::move(register_ep)) {} +CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) + : register_ep_(std::move(register_ep)), + installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP") {} + +CudaEpBootstrapper::~CudaEpBootstrapper() { +#if defined(__linux__) && !defined(__ANDROID__) + if (genai_cuda_handle_) { + dlclose(genai_cuda_handle_); + } +#endif +} const std::string& CudaEpBootstrapper::Name() const { return name_; @@ -75,14 +190,10 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, attempts_++; - auto ep_dir = std::filesystem::path(ep_dir_); - auto lock_path = ep_dir.parent_path() / kLockFileName; - auto zip_path = ep_dir.parent_path() / kPackageFileName; - try { auto override_path = Utils::GetEnv(kCudaProviderOverrideEnv); if (override_path.has_value() && !override_path->empty()) { - std::filesystem::path provider_path(*override_path); + std::filesystem::path provider_path = std::filesystem::absolute(*override_path); if (!std::filesystem::exists(provider_path)) { logger.Log(LogLevel::Warning, @@ -95,9 +206,11 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, progress_cb(name_, 90.0f); } - // Prepend the override directory to PATH so sibling dependency DLLs are discoverable, - // matching the normal install path. The provider DLL delay-loads CUDA/cuDNN dependencies. - PrependDirToProcessPath(provider_path.parent_path()); +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_handle_, logger)) { + return false; + } +#endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, @@ -118,97 +231,48 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, return true; } - // Cross-process lock to prevent concurrent installs - FileLock lock(lock_path); - - // Check if package already exists and is valid - if (fl::VerifyEpBinaries(ep_dir, - {{kExpectedBinaries[0].filename, kExpectedBinaries[0].sha256}, - {kExpectedBinaries[1].filename, kExpectedBinaries[1].sha256}}, - "CUDA EP", logger)) { - logger.Log(LogLevel::Information, "CUDA EP: package already valid, skipping download"); - } else { - // Clean up any partial install - if (std::filesystem::exists(ep_dir)) { - std::filesystem::remove_all(ep_dir); - } - - std::filesystem::create_directories(ep_dir); - - // Download - logger.Log(LogLevel::Information, "CUDA EP: downloading from CDN..."); - - // Bridge callback-based cancellation to the atomic flag HttpDownloadFile expects - std::atomic cancel_flag{false}; - - auto download_progress = [&](float pct) { - if (progress_cb) { - // 0-80% for download phase - if (!progress_cb(name_, pct * 0.8f)) { - cancel_flag.store(true); - } - } - }; - - if (!HttpDownloadFile(kDownloadUrl, zip_path, kUserAgent, - &cancel_flag, download_progress, logger)) { - logger.Log(LogLevel::Warning, "CUDA EP: download failed (see prior log for details)"); - return false; - } - - // Extract - logger.Log(LogLevel::Information, "CUDA EP: extracting..."); - - if (!ExtractZip(zip_path, ep_dir, logger)) { - logger.Log(LogLevel::Warning, "CUDA EP: extraction failed"); - return false; - } - - // Clean up zip - std::filesystem::remove(zip_path); - - // Verify - if (!fl::VerifyEpBinaries(ep_dir, - {{kExpectedBinaries[0].filename, kExpectedBinaries[0].sha256}, - {kExpectedBinaries[1].filename, kExpectedBinaries[1].sha256}}, - "CUDA EP", logger)) { - logger.Log(LogLevel::Warning, "CUDA EP: verification failed after download"); - return false; - } + auto manifest = BuildCudaManifest(); + if (!manifest.has_value()) { + logger.Log(LogLevel::Warning, "CUDA EP: no bundle available for this platform"); + return false; } - if (progress_cb) { - progress_cb(name_, 90.0f); + // CUDA force requests another registration attempt, but retains the existing package reuse behavior. + auto txn = installer_.EnsureInstalled(*manifest, progress_cb, logger, + EpBundleInstallPolicy::ReuseVerified); + if (!txn) { + return false; } - // Register with ORT + auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + #ifdef _WIN32 - // Permanently prepend the EP directory to PATH. The zip bundles all - // required CUDA/cuDNN DLLs, so no system CUDA install is needed. - // PATH must stay modified for the process lifetime because: - // - onnxruntime_providers_cuda.dll delay-loads some dependencies - // - onnxruntime-genai-cuda.dll is loaded later at model-load time - // - ORT creates CUDA sessions after registration - PrependDirToProcessPath(ep_dir); + if (!LoadBundleDependencies(txn->bin_dir(), *manifest, logger)) { + return false; + } +#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + if (!LoadGenAiCudaLibrary(txn->bin_dir() / kGenAiCudaLibrary, genai_cuda_handle_, logger)) { + return false; + } #endif - auto cuda_dll_path = ep_dir / kCudaProviderDll; - - if (!register_ep_(kRegistrationName, cuda_dll_path)) { + if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "CUDA EP: ORT registration failed"); return false; } registered_ = true; + if (!txn->CommitActive(logger)) { + logger.Log(LogLevel::Warning, "CUDA EP: failed to publish active bundle marker"); + } + if (progress_cb) { progress_cb(name_, 100.0f); } - // Bootstrapper-side log — captures the install dir, which the central - // register_ep callback (logs library + version) doesn't have. logger.Log(LogLevel::Information, - fmt::format("CUDA EP: ready (install_path={})", ep_dir.string())); + fmt::format("CUDA EP: ready (install_path={})", txn->bin_dir().string())); return true; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("CUDA EP: error: {}", e.what())); @@ -217,39 +281,16 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, } bool CudaEpBootstrapper::HasNvidiaGpu() { -#ifdef _WIN32 - FILE* pipe = _popen("nvidia-smi --query-gpu=compute_cap --format=csv,noheader,nounits 2>nul", "r"); -#else - FILE* pipe = popen("nvidia-smi --query-gpu=compute_cap --format=csv,noheader,nounits 2>/dev/null", "r"); -#endif - - if (!pipe) { - return false; - } - - char buffer[128]; - std::string result; - while (fgets(buffer, sizeof(buffer), pipe)) { - result += buffer; - } + return NvmlGpuDetector::HasNvidiaGpu(); +} -#ifdef _WIN32 - int exit_code = _pclose(pipe); +bool CudaEpBootstrapper::IsSupportedPlatform() { +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) + return true; #else - int exit_code = pclose(pipe); + return false; #endif - - if (exit_code != 0 || result.empty()) { - return false; - } - - // Need compute capability >= 5.0 for CUDA 12 - try { - float compute_cap = std::stof(result); - return compute_cap >= 5.0f; - } catch (...) { - return false; - } } } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index b4120d53b..d533d314b 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -3,6 +3,7 @@ #pragma once #include "ep_detection/ep_bootstrapper.h" +#include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" #include @@ -13,19 +14,13 @@ class ILogger; /// Bootstrapper for the CUDA execution provider. /// -/// Windows x64: downloads CUDA EP binaries from Azure CDN, extracts, -/// verifies SHA256, then registers with ORT via SetDllDirectory + callback. -/// -/// Linux x64: registers from co-located .so files (no download needed). -/// -/// Checks for NVIDIA GPU externally — only instantiate if NVIDIA GPU detected. +/// Installs and registers the CUDA execution provider. class CudaEpBootstrapper : public IEpBootstrapper { public: - /// @param ep_dir Base directory for EP packages (e.g., appdata/foundry-local). - /// The CUDA package will be at ep_dir/cuda-ep/. + /// @param root_dir Root directory for the CUDA EP bundle, e.g. "/ep/cuda-ep". /// @param register_ep Callback to register the EP DLL with ORT. - CudaEpBootstrapper(std::string ep_dir, EpRegistrationCallback register_ep); - ~CudaEpBootstrapper() override = default; + CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep); + ~CudaEpBootstrapper() override; // Non-copyable CudaEpBootstrapper(const CudaEpBootstrapper&) = delete; @@ -37,16 +32,21 @@ class CudaEpBootstrapper : public IEpBootstrapper { const ProgressCallback& progress_cb, ILogger& logger) override; - /// Check if an NVIDIA GPU with sufficient compute capability is present. - /// Shells out to nvidia-smi to check. Returns false if nvidia-smi is not found. + /// Check for an NVIDIA GPU with compute capability >= 5.0 using NVML. static bool HasNvidiaGpu(); + /// Whether Foundry Local publishes a CUDA EP bundle for this platform. + static bool IsSupportedPlatform(); + private: - std::string ep_dir_; std::string name_ = "CUDAExecutionProvider"; bool registered_ = false; int attempts_ = 0; EpRegistrationCallback register_ep_; + EpBundleInstaller installer_; +#if defined(__linux__) && !defined(__ANDROID__) + void* genai_cuda_handle_ = nullptr; +#endif }; } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc new file mode 100644 index 000000000..076256157 --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc @@ -0,0 +1,600 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/ep_bundle_installer.h" + +#include "http/http_download.h" +#include "logger.h" +#include "util/file_lock.h" +#include "util/sha256.h" +#include "util/string_utils.h" +#include "util/zip_extract.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace fl { + +namespace { + +constexpr int kArchiveHashRetries = 1; +constexpr int kRawHashRetries = 0; + +std::string GenerateUniqueId() { + static thread_local std::mt19937_64 rng( + std::random_device{}() ^ + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + return fmt::format("{:016x}", rng()); +} + +std::optional ReadActiveMarker(const std::filesystem::path& active_path) { + std::ifstream in(active_path); + if (!in) { + return std::nullopt; + } + + std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + while (!content.empty() && (content.back() == '\n' || content.back() == '\r' || content.back() == ' ')) { + content.pop_back(); + } + + return content.empty() ? std::nullopt : std::make_optional(content); +} + +bool AtomicReplaceFile(const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec) { +#ifdef _WIN32 + if (::MoveFileExW(from.wstring().c_str(), to.wstring().c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) { + ec.assign(static_cast(::GetLastError()), std::system_category()); + return false; + } + + ec.clear(); + return true; +#else + std::filesystem::rename(from, to, ec); + return !ec; +#endif +} + +bool PublishActiveMarker(const std::filesystem::path& root_dir, const std::string& generation_id, + std::string_view ep_display_name, ILogger& logger) { + const auto active_path = root_dir / "active"; + const auto tmp_path = root_dir / fmt::format("active.{}.tmp", GenerateUniqueId()); + + bool write_ok = false; + { + std::ofstream out(tmp_path, std::ios::binary | std::ios::trunc); + if (out) { + out << generation_id; + out.flush(); + out.close(); + write_ok = static_cast(out); // true only if the write, flush, and close all succeeded + } + } + + if (!write_ok) { + logger.Log(LogLevel::Warning, + fmt::format("{}: failed to write active marker temp file '{}'", ep_display_name, tmp_path.string())); + std::error_code ec; + std::filesystem::remove(tmp_path, ec); + return false; + } + + std::error_code ec; + if (!AtomicReplaceFile(tmp_path, active_path, ec)) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to publish active marker '{}': {}", ep_display_name, + active_path.string(), ec.message())); + std::filesystem::remove(tmp_path, ec); + return false; + } + + return true; +} + +bool IsSha256(std::string_view value) { + return value.size() == 64 && + std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isxdigit(ch) != 0; }); +} + +bool IsSafeId(std::string_view value) { + if (value.empty() || value == "." || value == "..") { + return false; + } + + return std::all_of(value.begin(), value.end(), [](unsigned char ch) { + return std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.'; + }); +} + +bool IsSafeRelativePath(std::string_view value) { + if (value.empty() || value.find('\\') != std::string_view::npos) { + return false; + } + + const std::filesystem::path path(value); + if (path.is_absolute() || path.has_root_path() || path.lexically_normal().generic_string() != value) { + return false; + } + + for (const auto& component : path) { + if (component == "." || component == "..") { + return false; + } + } + + return true; +} + +bool IsHttpsUrl(std::string_view value) { + constexpr std::string_view prefix = "https://"; + if (!value.starts_with(prefix) || value.find('#') != std::string_view::npos) { + return false; + } + + const auto authority_end = value.find('/', prefix.size()); + const auto authority = value.substr(prefix.size(), authority_end - prefix.size()); + return !authority.empty() && authority.find('@') == std::string_view::npos; +} + +bool ValidateManifest(const EpBundleManifest& manifest, std::string_view ep_display_name, ILogger& logger) { + if (!manifest.IsSupported()) { + logger.Log(LogLevel::Warning, + fmt::format("{}: bundle is disabled or incomplete", ep_display_name)); + return false; + } + + if (!IsSafeId(manifest.bundle_id) || !IsSafeRelativePath(manifest.provider_relative_path)) { + logger.Log(LogLevel::Warning, fmt::format("{}: bundle manifest contains an invalid path", ep_display_name)); + return false; + } + + std::unordered_set artifact_ids; + std::unordered_set file_paths; + + for (const auto& artifact : manifest.artifacts) { + if (!IsSafeId(artifact.id) || !artifact_ids.insert(artifact.id).second || !IsHttpsUrl(artifact.url)) { + logger.Log(LogLevel::Warning, fmt::format("{}: artifact metadata is invalid", ep_display_name)); + return false; + } + + if (artifact.is_archive) { + if (!IsSha256(artifact.archive_sha256) || artifact.archive_max_bytes == 0 || + artifact.extracted_files.empty()) { + logger.Log(LogLevel::Warning, fmt::format("{}: archive artifact metadata is incomplete", ep_display_name)); + return false; + } + + for (const auto& file : artifact.extracted_files) { + if (!IsSafeRelativePath(file.relative_path) || !IsSha256(file.sha256) || + !file_paths.insert(file.relative_path).second) { + logger.Log(LogLevel::Warning, fmt::format("{}: archive file metadata is invalid", ep_display_name)); + return false; + } + } + } else { + if (!IsSafeRelativePath(artifact.raw_relative_path) || !IsSha256(artifact.raw_sha256) || + artifact.raw_max_bytes == 0 || !file_paths.insert(artifact.raw_relative_path).second) { + logger.Log(LogLevel::Warning, fmt::format("{}: raw artifact metadata is invalid", ep_display_name)); + return false; + } + } + } + + if (file_paths.count(manifest.provider_relative_path) == 0) { + logger.Log(LogLevel::Warning, fmt::format("{}: provider is not present in the bundle manifest", ep_display_name)); + return false; + } + + return true; +} + +bool CollectRegularFiles(const std::filesystem::path& dir, std::unordered_set& files) { + std::error_code ec; + + if (!std::filesystem::exists(dir, ec)) { + return true; + } + + for (const auto& entry : std::filesystem::recursive_directory_iterator( + dir, std::filesystem::directory_options::skip_permission_denied, ec)) { + const auto status = entry.symlink_status(ec); + if (ec) { + return false; + } + + if (std::filesystem::is_regular_file(status)) { + files.insert(std::filesystem::relative(entry.path(), dir, ec).generic_string()); + } else if (!std::filesystem::is_directory(status)) { + return false; + } + } + + return !ec; +} + +bool VerifyRegularFile(const std::filesystem::path& path, std::string_view expected_hash) { + std::error_code ec; + const auto status = std::filesystem::symlink_status(path, ec); + return !ec && std::filesystem::is_regular_file(status) && + CompareCaseInsensitive(Sha256File(path), std::string(expected_hash)) == 0; +} + +bool VerifyArtifactFiles(const std::filesystem::path& bin_dir, const EpBundleArtifact& artifact) { + if (artifact.is_archive) { + return std::all_of(artifact.extracted_files.begin(), artifact.extracted_files.end(), [&](const auto& file) { + return VerifyRegularFile(bin_dir / file.relative_path, file.sha256); + }); + } + + return VerifyRegularFile(bin_dir / artifact.raw_relative_path, artifact.raw_sha256); +} + +bool CopyArtifactFiles(const std::filesystem::path& source_bin, const std::filesystem::path& staging_bin, + const EpBundleArtifact& artifact) { + std::vector paths; + if (artifact.is_archive) { + for (const auto& file : artifact.extracted_files) { + paths.push_back(file.relative_path); + } + } else { + paths.push_back(artifact.raw_relative_path); + } + + std::error_code ec; + for (const auto& relative_path : paths) { + const auto source = source_bin / relative_path; + const auto destination = staging_bin / relative_path; + std::filesystem::create_directories(destination.parent_path(), ec); + if (ec || !std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing, + ec)) { + return false; + } + } + + return VerifyArtifactFiles(staging_bin, artifact); +} + +bool VerifyBundleDir(const std::filesystem::path& bin_dir, const EpBundleManifest& manifest, + std::string_view ep_display_name, ILogger& logger) { + std::unordered_set expected; + + for (const auto& artifact : manifest.artifacts) { + if (artifact.is_archive) { + for (const auto& file : artifact.extracted_files) { + expected.insert(file.relative_path); + } + } else { + expected.insert(artifact.raw_relative_path); + } + + if (!VerifyArtifactFiles(bin_dir, artifact)) { + logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' has invalid files for artifact '{}'", + ep_display_name, manifest.bundle_id, artifact.id)); + return false; + } + } + + std::unordered_set actual; + if (!CollectRegularFiles(bin_dir, actual)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: bundle '{}' contains an unsupported filesystem entry", ep_display_name, + manifest.bundle_id)); + return false; + } + + if (actual.size() != expected.size()) { + logger.Log(LogLevel::Warning, + fmt::format("{}: bundle '{}' contains unexpected files ({} present, {} expected)", ep_display_name, + manifest.bundle_id, actual.size(), expected.size())); + return false; + } + + return true; +} + +void CleanupStaleGenerations(const std::filesystem::path& bundles_dir, const std::filesystem::path& staging_root, + const std::unordered_set& keep_ids, std::string_view ep_display_name, + ILogger& logger) { + std::error_code ec; + + if (std::filesystem::exists(staging_root, ec)) { + for (const auto& entry : std::filesystem::directory_iterator(staging_root, ec)) { + std::filesystem::remove_all(entry.path(), ec); + } + } + + if (!std::filesystem::exists(bundles_dir, ec)) { + return; + } + + for (const auto& entry : std::filesystem::directory_iterator(bundles_dir, ec)) { + if (keep_ids.count(entry.path().filename().string()) == 0) { + logger.Log(LogLevel::Debug, + fmt::format("{}: removing orphaned bundle generation '{}'", ep_display_name, + entry.path().filename().string())); + std::filesystem::remove_all(entry.path(), ec); + } + } +} + +void HardenFilePermissions(const std::filesystem::path& path) { + std::error_code ec; + std::filesystem::permissions(path, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | std::filesystem::perms::others_read, + std::filesystem::perm_options::replace, ec); +} + +bool DownloadWithHashRetry(const EpArtifactDownloadFn& download_fn, const std::string& url, + const std::filesystem::path& destination, uint64_t max_bytes, + const std::string& expected_sha256, int max_retries, std::string_view artifact_id, + std::string_view ep_display_name, float base_pct, float span_pct, + const IEpBootstrapper::ProgressCallback& progress_cb, ILogger& logger) { + for (int attempt = 0; attempt <= max_retries; ++attempt) { + std::error_code ec; + std::filesystem::remove(destination, ec); + + std::atomic cancel_flag{false}; + auto local_progress = [&](float pct) { + if (progress_cb && !progress_cb(std::string(ep_display_name), base_pct + pct * span_pct / 100.0f)) { + cancel_flag.store(true); + } + }; + + if (!download_fn(url, destination, max_bytes, &cancel_flag, local_progress, logger)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: download failed for artifact '{}'", ep_display_name, artifact_id)); + return false; + } + + auto hash = Sha256File(destination); + if (CompareCaseInsensitive(hash, expected_sha256) == 0) { + return true; + } + + logger.Log(LogLevel::Warning, + fmt::format("{}: hash mismatch for artifact '{}' (attempt {}/{}): got {}, expected {}", + ep_display_name, artifact_id, attempt + 1, max_retries + 1, hash, expected_sha256)); + } + + return false; +} + +bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBundleArtifact& artifact, + const std::filesystem::path& staging_dir, const std::filesystem::path& staging_bin, + float base_pct, float span_pct, std::string_view ep_display_name, + const IEpBootstrapper::ProgressCallback& progress_cb, ILogger& logger) { + auto archive_path = staging_dir / (artifact.id + ".archive"); + + if (!DownloadWithHashRetry(download_fn, artifact.url, archive_path, artifact.archive_max_bytes, + artifact.archive_sha256, kArchiveHashRetries, artifact.id, ep_display_name, base_pct, + span_pct, progress_cb, logger)) { + return false; + } + + if (!ExtractZip(archive_path, staging_bin, logger)) { + logger.Log(LogLevel::Warning, fmt::format("{}: extraction failed for artifact '{}'", ep_display_name, artifact.id)); + return false; + } + + std::error_code ec; + std::filesystem::remove(archive_path, ec); + + for (const auto& file : artifact.extracted_files) { + auto file_path = staging_bin / file.relative_path; + if (!std::filesystem::is_regular_file(file_path, ec)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: artifact '{}' is missing expected extracted file '{}'", ep_display_name, + artifact.id, file.relative_path)); + return false; + } + + auto hash = Sha256File(file_path); + if (CompareCaseInsensitive(hash, file.sha256) != 0) { + logger.Log(LogLevel::Warning, + fmt::format("{}: extracted file '{}' from artifact '{}' hash mismatch: got {}, expected {}", + ep_display_name, file.relative_path, artifact.id, hash, file.sha256)); + return false; + } + + HardenFilePermissions(file_path); + } + + return true; +} + +bool InstallRawArtifact(const EpArtifactDownloadFn& download_fn, const EpBundleArtifact& artifact, + const std::filesystem::path& staging_bin, float base_pct, float span_pct, + std::string_view ep_display_name, const IEpBootstrapper::ProgressCallback& progress_cb, + ILogger& logger) { + auto raw_path = staging_bin / artifact.raw_relative_path; + std::filesystem::create_directories(raw_path.parent_path()); + + if (!DownloadWithHashRetry(download_fn, artifact.url, raw_path, artifact.raw_max_bytes, artifact.raw_sha256, + kRawHashRetries, artifact.id, ep_display_name, base_pct, span_pct, progress_cb, logger)) { + return false; + } + + HardenFilePermissions(raw_path); + return true; +} + +EpArtifactDownloadFn DefaultDownloadFn() { + return [](const std::string& url, const std::filesystem::path& destination, uint64_t max_bytes, + std::atomic* cancel_flag, const std::function& progress_cb, ILogger& logger) { + const int64_t cap = max_bytes == 0 ? -1 : static_cast(max_bytes); + return HttpDownloadFile(url, destination, "FoundryLocal", cancel_flag, progress_cb, logger, cap); + }; +} + +} // namespace + +EpBundleInstaller::EpBundleInstaller(std::filesystem::path root_dir, std::string lock_file_name, + std::string ep_display_name, EpArtifactDownloadFn download_fn) + : root_dir_(std::filesystem::absolute(std::move(root_dir))), + lock_file_name_(std::move(lock_file_name)), + ep_display_name_(std::move(ep_display_name)), + download_fn_(download_fn ? std::move(download_fn) : DefaultDownloadFn()) {} + +std::unique_ptr EpBundleInstaller::EnsureInstalled( + const EpBundleManifest& manifest, const IEpBootstrapper::ProgressCallback& progress_cb, ILogger& logger, + EpBundleInstallPolicy policy) { + if (!ValidateManifest(manifest, ep_display_name_, logger)) { + return nullptr; + } + + try { + std::filesystem::create_directories(root_dir_); + auto lock = std::make_unique(root_dir_ / lock_file_name_); + + auto bundles_dir = root_dir_ / "bundles"; + auto staging_root = root_dir_ / "staging"; + auto active_generation = ReadActiveMarker(root_dir_ / "active"); + + std::unordered_set keep_ids; + if (active_generation.has_value() && IsSafeId(*active_generation)) { + keep_ids.insert(*active_generation); + } else { + active_generation.reset(); + } + CleanupStaleGenerations(bundles_dir, staging_root, keep_ids, ep_display_name_, logger); + + std::filesystem::path active_bin; + if (active_generation.has_value()) { + active_bin = bundles_dir / *active_generation / "bin"; + } + + if (policy == EpBundleInstallPolicy::ReuseVerified && !active_bin.empty() && + VerifyBundleDir(active_bin, manifest, ep_display_name_, logger)) { + logger.Log(LogLevel::Information, + fmt::format("{}: reusing verified bundle '{}'", ep_display_name_, manifest.bundle_id)); + if (progress_cb) { + progress_cb(ep_display_name_, 90.0f); + } + return std::unique_ptr( + new EpInstallTransaction(std::move(lock), root_dir_, ep_display_name_, manifest, *active_generation, + active_bin)); + } + + auto staging_dir = staging_root / GenerateUniqueId(); + auto staging_bin = staging_dir / "bin"; + std::filesystem::create_directories(staging_bin); + + logger.Log(LogLevel::Information, + fmt::format("{}: installing bundle '{}'", ep_display_name_, manifest.bundle_id)); + + const size_t artifact_count = manifest.artifacts.size(); + for (size_t i = 0; i < artifact_count; ++i) { + const auto& artifact = manifest.artifacts[i]; + const float base_pct = (static_cast(i) / static_cast(artifact_count)) * 80.0f; + const float span_pct = 80.0f / static_cast(artifact_count); + + bool ok = false; + if (policy == EpBundleInstallPolicy::ReuseVerified && !active_bin.empty() && + VerifyArtifactFiles(active_bin, artifact)) { + ok = CopyArtifactFiles(active_bin, staging_bin, artifact); + } else { + ok = artifact.is_archive + ? InstallArchiveArtifact(download_fn_, artifact, staging_dir, staging_bin, base_pct, span_pct, + ep_display_name_, progress_cb, logger) + : InstallRawArtifact(download_fn_, artifact, staging_bin, base_pct, span_pct, ep_display_name_, + progress_cb, logger); + } + + if (!ok) { + std::error_code ec; + std::filesystem::remove_all(staging_dir, ec); + return nullptr; + } + } + + if (!VerifyBundleDir(staging_bin, manifest, ep_display_name_, logger)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: staged bundle '{}' failed verification before publish", ep_display_name_, + manifest.bundle_id)); + std::error_code ec; + std::filesystem::remove_all(staging_dir, ec); + return nullptr; + } + + std::filesystem::create_directories(bundles_dir); + const auto generation_id = manifest.bundle_id + "-" + GenerateUniqueId(); + auto final_bundle_dir = bundles_dir / generation_id; + std::filesystem::rename(staging_dir, final_bundle_dir); + + logger.Log(LogLevel::Information, + fmt::format("{}: installed bundle '{}'", ep_display_name_, manifest.bundle_id)); + + if (progress_cb) { + progress_cb(ep_display_name_, 90.0f); + } + + return std::unique_ptr( + new EpInstallTransaction(std::move(lock), root_dir_, ep_display_name_, manifest, generation_id, + final_bundle_dir / "bin")); + } catch (const std::exception& e) { + logger.Log(LogLevel::Warning, fmt::format("{}: install error: {}", ep_display_name_, e.what())); + return nullptr; + } +} + +EpInstallTransaction::EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, + std::string ep_display_name, EpBundleManifest manifest, + std::string generation_id, std::filesystem::path bin_dir) + : lock_(std::move(lock)), + root_dir_(std::move(root_dir)), + ep_display_name_(std::move(ep_display_name)), + manifest_(std::move(manifest)), + generation_id_(std::move(generation_id)), + bin_dir_(std::move(bin_dir)) {} + +EpInstallTransaction::~EpInstallTransaction() = default; + +bool EpInstallTransaction::CommitActive(ILogger& logger) { + if (committed_) { + return true; + } + + try { + if (!VerifyBundleDir(bin_dir_, manifest_, ep_display_name_, logger)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: bundle '{}' failed re-verification before activation; active marker unchanged", + ep_display_name_, manifest_.bundle_id)); + return false; + } + + if (!PublishActiveMarker(root_dir_, generation_id_, ep_display_name_, logger)) { + return false; + } + + committed_ = true; + CleanupStaleGenerations(root_dir_ / "bundles", root_dir_ / "staging", {generation_id_}, ep_display_name_, + logger); + return true; + } catch (const std::exception& e) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to commit active bundle: {}", ep_display_name_, e.what())); + return false; + } +} + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h new file mode 100644 index 000000000..66cfc8362 --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include "ep_detection/ep_bootstrapper.h" +#include "ep_detection/ep_bundle_manifest.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fl { + +class FileLock; +class ILogger; + +using EpArtifactDownloadFn = std::function* cancel_flag, + const std::function& progress_cb, + ILogger& logger)>; + +enum class EpBundleInstallPolicy { + ReuseVerified, + ForceDownload, +}; + +class EpInstallTransaction { + public: + ~EpInstallTransaction(); + + EpInstallTransaction(const EpInstallTransaction&) = delete; + EpInstallTransaction& operator=(const EpInstallTransaction&) = delete; + EpInstallTransaction(EpInstallTransaction&&) = delete; + EpInstallTransaction& operator=(EpInstallTransaction&&) = delete; + + const std::filesystem::path& bin_dir() const { return bin_dir_; } + + const std::string& bundle_id() const { return manifest_.bundle_id; } + + bool CommitActive(ILogger& logger); + + private: + friend class EpBundleInstaller; + + EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, + std::string ep_display_name, EpBundleManifest manifest, std::string generation_id, + std::filesystem::path bin_dir); + + std::unique_ptr lock_; + std::filesystem::path root_dir_; + std::string ep_display_name_; + EpBundleManifest manifest_; + std::string generation_id_; + std::filesystem::path bin_dir_; + bool committed_ = false; +}; + +class EpBundleInstaller { + public: + EpBundleInstaller(std::filesystem::path root_dir, + std::string lock_file_name, + std::string ep_display_name, + EpArtifactDownloadFn download_fn = nullptr); + + std::unique_ptr EnsureInstalled(const EpBundleManifest& manifest, + const IEpBootstrapper::ProgressCallback& progress_cb, + ILogger& logger, + EpBundleInstallPolicy policy = + EpBundleInstallPolicy::ReuseVerified); + + private: + std::filesystem::path root_dir_; + std::string lock_file_name_; + std::string ep_display_name_; + EpArtifactDownloadFn download_fn_; +}; + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h new file mode 100644 index 000000000..3f1eb93ab --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include + +namespace fl { + +struct EpBundleFile { + std::string relative_path; + std::string sha256; +}; + +struct EpBundleArtifact { + std::string id; + std::string url; + bool is_archive = true; + + std::string archive_sha256; + std::vector extracted_files; + uint64_t archive_max_bytes = 0; + + std::string raw_relative_path; + std::string raw_sha256; + uint64_t raw_max_bytes = 0; + + bool IsEnabled() const { return !url.empty(); } +}; + +struct EpBundleManifest { + std::string bundle_id; + std::vector artifacts; + std::string provider_relative_path; + + bool IsSupported() const { + if (bundle_id.empty() || artifacts.empty() || provider_relative_path.empty()) { + return false; + } + + for (const auto& artifact : artifacts) { + if (!artifact.IsEnabled()) { + return false; + } + } + + return true; + } +}; + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc new file mode 100644 index 000000000..c1dab2e1a --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/nvml_gpu_detector.h" + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif + +namespace fl { + +namespace { + +using NvmlDevice = void*; +constexpr int kNvmlSuccess = 0; + +using NvmlInitFn = int (*)(); +using NvmlShutdownFn = int (*)(); +using NvmlDeviceGetCountFn = int (*)(unsigned int*); +using NvmlDeviceGetHandleByIndexFn = int (*)(unsigned int, NvmlDevice*); +using NvmlDeviceGetCudaComputeCapabilityFn = int (*)(NvmlDevice, int*, int*); + +#ifdef _WIN32 + +using LibraryHandle = HMODULE; +constexpr LibraryHandle kNullLibrary = nullptr; + +LibraryHandle LoadNvmlFromProgramFiles(REFKNOWNFOLDERID folder_id) { + PWSTR program_files = nullptr; + if (SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, nullptr, &program_files) != S_OK) { + return nullptr; + } + + const std::filesystem::path path = + std::filesystem::path(program_files) / L"NVIDIA Corporation" / L"NVSMI" / L"nvml.dll"; + CoTaskMemFree(program_files); + return LoadLibraryExW(path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); +} + +LibraryHandle LoadNvmlLibrary() { + if (auto library = LoadLibraryExW(L"nvml.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)) { + return library; + } + + if (auto library = LoadNvmlFromProgramFiles(FOLDERID_ProgramFiles)) { + return library; + } + +#if defined(_M_ARM64) + return nullptr; +#else + return LoadNvmlFromProgramFiles(FOLDERID_ProgramFilesX64); +#endif +} + +void* GetSymbol(LibraryHandle lib, const char* name) { + return reinterpret_cast(GetProcAddress(lib, name)); +} + +void UnloadLibrary(LibraryHandle lib) { + if (lib) { + FreeLibrary(lib); + } +} + +#else + +using LibraryHandle = void*; +constexpr LibraryHandle kNullLibrary = nullptr; + +LibraryHandle LoadNvmlLibrary() { + return dlopen("libnvidia-ml.so.1", RTLD_NOW | RTLD_LOCAL); +} + +void* GetSymbol(LibraryHandle lib, const char* name) { + return dlsym(lib, name); +} + +void UnloadLibrary(LibraryHandle lib) { + if (lib) { + dlclose(lib); + } +} + +#endif + +class NvmlLibrary { + public: + NvmlLibrary() { + lib_ = LoadNvmlLibrary(); + if (!lib_) { + return; + } + + init_ = reinterpret_cast(GetSymbol(lib_, "nvmlInit_v2")); + shutdown_ = reinterpret_cast(GetSymbol(lib_, "nvmlShutdown")); + get_count_ = reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetCount_v2")); + get_handle_ = + reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetHandleByIndex_v2")); + get_compute_cap_ = reinterpret_cast( + GetSymbol(lib_, "nvmlDeviceGetCudaComputeCapability")); + + if (!init_ || !shutdown_ || !get_count_ || !get_handle_ || !get_compute_cap_) { + UnloadLibrary(lib_); + lib_ = kNullLibrary; + return; + } + + initialized_ = (init_() == kNvmlSuccess); + } + + ~NvmlLibrary() { + if (initialized_ && shutdown_) { + shutdown_(); + } + UnloadLibrary(lib_); + } + + NvmlLibrary(const NvmlLibrary&) = delete; + NvmlLibrary& operator=(const NvmlLibrary&) = delete; + + bool IsReady() const { return lib_ != kNullLibrary && initialized_; } + + std::vector> QueryComputeCapabilities() const { + std::vector> result; + if (!IsReady()) { + return result; + } + + unsigned int count = 0; + if (get_count_(&count) != kNvmlSuccess || count == 0) { + return result; + } + + for (unsigned int i = 0; i < count; ++i) { + NvmlDevice device = nullptr; + if (get_handle_(i, &device) != kNvmlSuccess) { + continue; + } + + int major = 0; + int minor = 0; + if (get_compute_cap_(device, &major, &minor) == kNvmlSuccess) { + result.emplace_back(major, minor); + } + } + + return result; + } + + private: + LibraryHandle lib_ = kNullLibrary; + bool initialized_ = false; + NvmlInitFn init_ = nullptr; + NvmlShutdownFn shutdown_ = nullptr; + NvmlDeviceGetCountFn get_count_ = nullptr; + NvmlDeviceGetHandleByIndexFn get_handle_ = nullptr; + NvmlDeviceGetCudaComputeCapabilityFn get_compute_cap_ = nullptr; +}; + +} // namespace + +bool HasQualifyingComputeCapability(const std::vector>& capabilities, + int min_major, + int min_minor) { + for (const auto& [major, minor] : capabilities) { + if (major > min_major || (major == min_major && minor >= min_minor)) { + return true; + } + } + + return false; +} + +bool NvmlGpuDetector::HasNvidiaGpu() { + NvmlLibrary nvml; + if (!nvml.IsReady()) { + return false; + } + + return HasQualifyingComputeCapability(nvml.QueryComputeCapabilities()); +} + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h new file mode 100644 index 000000000..2a59f8db9 --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include +#include + +namespace fl { + +bool HasQualifyingComputeCapability(const std::vector>& capabilities, + int min_major = 5, + int min_minor = 0); + +class NvmlGpuDetector { + public: + static bool HasNvidiaGpu(); +}; + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index d8b63db52..9c5294915 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -3,118 +3,104 @@ #include "ep_detection/webgpu_ep_bootstrapper.h" #include "ep_detection/ep_utils.h" -#include "http/http_download.h" #include "logger.h" -#include "util/file_lock.h" -#include "util/sha256.h" #include "utils.h" -#include "util/zip_extract.h" #include -#include -#include -#include +#include #include -#include +#include #include -#include -#include namespace { -constexpr const char* kPackageFileName = "webgpu-ep.zip"; constexpr const char* kLockFileName = "webgpu-ep.lock"; -constexpr const char* kStagingDirName = "webgpu-ep-staging"; -constexpr const char* kUserAgent = "FoundryLocal"; constexpr int kMaxInstallAttempts = 5; - -struct WebGpuPackageMetadata { - const char* download_url; - const char* zip_sha256; - const char* provider_sha256; -#if defined(_WIN32) - const char* dxcompiler_sha256; - const char* dxil_sha256; -#endif -}; - -#if defined(_WIN32) -const std::unordered_map kPackageMetadata = { - {"win-arm64", - {"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.1.0_win-arm64.zip", - "90CD6744103F29530C9B1467583543F9460C27F67DF185A8352EA1C7A29DC8F8", - "C4A77911BDBFC6E2870D1895DA3F5BE476CE3398D772C02AFDDD2B2C49C66659", - "6DA88B1B24EAF5A7E0CAD46A1A41B9D26FF5244464544986AF85385F6C04F807", - "60AC8AECDAC6D509CCEF67E127E295EC90BE6CCABDB84606D79B7A1B208082CA"} - }, - {"win-x64", - {"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.1.0_win-x64.zip", - "CCD9ADB663D670069BB1369FA7319BB2C91180BA3262CA4000F14DA066AF0059", - "591E286A211B133E3C4E5C833FEBF2D594B7B548433A2490407B11B906A9271B", - "2CEC74EF87A1171E4502C7D735169756BF4C528676EA354BFDE24B01394965F0", - "0767448ABEADE590821E88E56C471E0F2DFE89EC9A7642D08ADC3DC94F14AB92"} - }, -}; -#elif defined(__APPLE__) -const std::unordered_map kPackageMetadata = { - {"macos-arm64", - {"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.1.0_macos-arm64.zip", - "E4BC0329EE60408A9E7183A9A5AA1D1639CB4C61C4C501A499B5911CFFC4DBF4", - "A08BCEBE097B555E23938FCC71A5FAAD461F586CAB0B63DC9D21E970F6CA4C87"} - }, -}; -#else -const std::unordered_map kPackageMetadata = { - {"linux-x64", - {"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.1.0_linux-x64.zip", - "B3241E6C33FDA7A0D507A3D8256DB432244F9DF1D87A2ACFD6EE470625EB5C76", - "CBDFF74E6569E3CF66B46F0D194D87CD3CF49E83B7AA46552C39B0218D58B215"} - }, -}; -#endif - -// Platform-specific package metadata is baked into the binary to keep -// verification inputs fixed at build time. #if defined(_WIN32) && defined(_M_ARM64) -constexpr const char* kPlatformKey = "win-arm64"; -#elif defined(_WIN32) -constexpr const char* kPlatformKey = "win-x64"; -#elif defined(__APPLE__) -constexpr const char* kPlatformKey = "macos-arm64"; -#else -constexpr const char* kPlatformKey = "linux-x64"; -#endif - -const WebGpuPackageMetadata& GetPackageMetadata() { - auto it = kPackageMetadata.find(kPlatformKey); - - if (it == kPackageMetadata.end()) { - throw std::runtime_error( - fmt::format("WebGPU EP: no package metadata configured for platform '{}'", kPlatformKey)); - } - - return it->second; -} - -// Platform-specific EP library filename. -#if defined(_WIN32) -constexpr const char* kWebGpuProviderLib = "onnxruntime_providers_webgpu.dll"; -#elif defined(__APPLE__) -constexpr const char* kWebGpuProviderLib = "libonnxruntime_providers_webgpu.dylib"; -#else -constexpr const char* kWebGpuProviderLib = "libonnxruntime_providers_webgpu.so"; +constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-arm64"; +constexpr const char* kDownloadUrl = + "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-arm64.zip"; +constexpr const char* kArchiveSha256 = "3674C8BD50F19AB84D3F738AC426DB37EB119BC1B790525B5A4F4139C253AF08"; +constexpr const char* kProviderSha256 = "63CFEF0E7FB8FDC2238F69CD8E804F50FDA393B2B60C448DAEC73E031DE75058"; +constexpr const char* kDxCompilerSha256 = "3895C1F437E8E91A771F562AD2E5EA9EF918365EA1D7D4216AF4C58BA87E9D7B"; +constexpr const char* kDxilSha256 = "9377B286B378AF2ACD7DA7686F25FB60C7D22DEC4BA384BAB0523494DE3E75D0"; +#elif defined(_WIN32) && defined(_M_X64) +constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-x64"; +constexpr const char* kDownloadUrl = + "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-x64.zip"; +constexpr const char* kArchiveSha256 = "91A05B2C9EAF326011FE74604BBDF06E08C5B95A1F40425F4426EF0E90A9984D"; +constexpr const char* kProviderSha256 = "BE2EBCC0A96D1558D9123C04E75C2851260FE45C9DBC8959CB2CD8D11B83ABBE"; +constexpr const char* kDxCompilerSha256 = "174DBC3DF8F7AF5C32C0E39F43C0D5BC576395EDC3CCDD64119A1B63C081ED55"; +constexpr const char* kDxilSha256 = "080C02F62E90D0AB7ACC463BBC10280C37397DC6D036224D7C10F2ED9C20E13D"; +#elif defined(__APPLE__) && defined(__aarch64__) +constexpr const char* kBundleId = "webgpu-ep-0.2.1-macos-arm64"; +constexpr const char* kDownloadUrl = + "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_macos-arm64.zip"; +constexpr const char* kArchiveSha256 = "5F0F8378172F53EFA281328F33D1EBC793E44197FFB4CC605D02C593B891C0EA"; +constexpr const char* kProviderSha256 = "8FAC874A60F32F0127C74CB7DEF915807FCC8A6C30B77629E45F8CEE60272EAE"; #endif constexpr const char* kRegistrationName = "Foundry.WebGPU"; constexpr const char* kWebGpuProviderOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; +constexpr const char* kVersionSha256 = "4CB81DA21A42BC8A1DE985A2C6C7DFEE3F634576B0C8C7FA0990FB027F1BB082"; +constexpr uint64_t kArchiveMaxBytes = 64ULL * 1024 * 1024; + +std::optional BuildWebGpuManifest() { +#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) + fl::EpBundleManifest manifest; + manifest.bundle_id = kBundleId; + manifest.provider_relative_path = "onnxruntime_providers_webgpu.dll"; + manifest.artifacts = {{ + .id = "webgpu-ep", + .url = kDownloadUrl, + .is_archive = true, + .archive_sha256 = kArchiveSha256, + .extracted_files = + { + {.relative_path = "dxcompiler.dll", .sha256 = kDxCompilerSha256}, + {.relative_path = "dxil.dll", .sha256 = kDxilSha256}, + {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = kProviderSha256}, + {.relative_path = "version.json", .sha256 = kVersionSha256}, + }, + .archive_max_bytes = kArchiveMaxBytes, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }}; + return manifest; +#elif defined(__APPLE__) && defined(__aarch64__) + fl::EpBundleManifest manifest; + manifest.bundle_id = kBundleId; + manifest.provider_relative_path = "libonnxruntime_providers_webgpu.dylib"; + manifest.artifacts = {{ + .id = "webgpu-ep", + .url = kDownloadUrl, + .is_archive = true, + .archive_sha256 = kArchiveSha256, + .extracted_files = + { + {.relative_path = "libonnxruntime_providers_webgpu.dylib", .sha256 = kProviderSha256}, + {.relative_path = "version.json", .sha256 = kVersionSha256}, + }, + .archive_max_bytes = kArchiveMaxBytes, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }}; + return manifest; +#else + return std::nullopt; +#endif +} } // anonymous namespace namespace fl { -WebGpuEpBootstrapper::WebGpuEpBootstrapper(std::string ep_dir, EpRegistrationCallback register_ep) - : ep_dir_(std::move(ep_dir)), register_ep_(std::move(register_ep)) {} +WebGpuEpBootstrapper::WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) + : register_ep_(std::move(register_ep)), + installer_(std::filesystem::path(root_dir), kLockFileName, "WebGPU EP") {} const std::string& WebGpuEpBootstrapper::Name() const { return name_; @@ -141,26 +127,10 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, attempts_++; - auto ep_dir = std::filesystem::path(ep_dir_); - auto parent_dir = ep_dir.parent_path(); - try { - const auto& package_metadata = GetPackageMetadata(); - #if defined(_WIN32) - const auto expected_files = std::initializer_list>{ - {kWebGpuProviderLib, package_metadata.provider_sha256}, - {"dxcompiler.dll", package_metadata.dxcompiler_sha256}, - {"dxil.dll", package_metadata.dxil_sha256}, - }; - #else - const auto expected_files = std::initializer_list>{ - {kWebGpuProviderLib, package_metadata.provider_sha256}, - }; - #endif - auto override_path = Utils::GetEnv(kWebGpuProviderOverrideEnv); if (override_path.has_value() && !override_path->empty()) { - std::filesystem::path provider_path(*override_path); + std::filesystem::path provider_path = std::filesystem::absolute(*override_path); if (!std::filesystem::exists(provider_path)) { logger.Log(LogLevel::Warning, @@ -196,135 +166,26 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, return true; } - // Check if package already exists and is valid - logger.Log(LogLevel::Debug, - fmt::format("WebGPU EP: verifying existing install at {}", ep_dir.string())); - - if (!force && VerifyEpBinaries( - ep_dir, - expected_files, - "WebGPU EP", - logger)) { - logger.Log(LogLevel::Debug, "WebGPU EP: local binaries match expected hashes, skipping download"); - } else { - // Ensure parent directory exists for the lock file - std::filesystem::create_directories(parent_dir); - auto lock_path = parent_dir / kLockFileName; - - // Cross-process lock to prevent concurrent installs - FileLock lock(lock_path); - - // Re-check after acquiring lock (another process may have completed the update) - if (!force && VerifyEpBinaries( - ep_dir, - expected_files, - "WebGPU EP", - logger)) { - logger.Log(LogLevel::Debug, "WebGPU EP: another process already completed the update"); - } else { - // Download and extract to staging directory for atomic swap - auto staging_dir = parent_dir / kStagingDirName; - if (std::filesystem::exists(staging_dir)) { - std::filesystem::remove_all(staging_dir); - } - std::filesystem::create_directories(staging_dir); - - auto zip_path = staging_dir / kPackageFileName; - - // Download - logger.Log(LogLevel::Information, - fmt::format("WebGPU EP: downloading for {} from CDN", kPlatformKey)); - - std::atomic cancel_flag{false}; - auto download_progress = [&](float pct) { - if (progress_cb) { - // 0–80% for download phase - if (!progress_cb(name_, pct * 0.8f)) { - cancel_flag.store(true); - } - } - }; - - if (!HttpDownloadFile(package_metadata.download_url, zip_path, kUserAgent, - &cancel_flag, download_progress, logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: download failed (see prior log for details)"); - return false; - } - - logger.Log(LogLevel::Debug, - fmt::format("WebGPU EP: verifying downloaded archive {}", zip_path.string())); - - if (!VerifyEpArchive(zip_path, - package_metadata.zip_sha256, - "WebGPU EP", - logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: downloaded archive verification failed"); - std::filesystem::remove_all(staging_dir); - return false; - } - - logger.Log(LogLevel::Debug, "WebGPU EP: downloaded archive verification succeeded"); - - // Extract - logger.Log(LogLevel::Information, - fmt::format("WebGPU EP: extracting package to {}", staging_dir.string())); - - if (!ExtractZip(zip_path, staging_dir, logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: extraction failed"); - std::filesystem::remove_all(staging_dir); - return false; - } - - // Clean up zip - std::filesystem::remove(zip_path); - - // Verify staging - logger.Log(LogLevel::Debug, - fmt::format("WebGPU EP: verifying extracted staging contents at {}", - staging_dir.string())); - - if (!VerifyEpBinaries( - staging_dir, - expected_files, - "WebGPU EP", - logger)) { - logger.Log(LogLevel::Warning, - fmt::format("WebGPU EP: verification failed after extraction (attempt {})", - attempts_)); - std::filesystem::remove_all(staging_dir); - return false; - } - - logger.Log(LogLevel::Debug, "WebGPU EP: staging verification succeeded"); - - // Atomic swap: delete old, rename staging to target - if (std::filesystem::exists(ep_dir)) { - std::filesystem::remove_all(ep_dir); - } - - std::filesystem::rename(staging_dir, ep_dir); - - logger.Log(LogLevel::Information, "WebGPU EP: successfully installed"); - } + auto manifest = BuildWebGpuManifest(); + if (!manifest.has_value()) { + logger.Log(LogLevel::Warning, "WebGPU EP: no bundle available for this platform"); + return false; } - if (progress_cb) { - progress_cb(name_, 90.0f); + const auto install_policy = + force ? EpBundleInstallPolicy::ForceDownload : EpBundleInstallPolicy::ReuseVerified; + auto txn = installer_.EnsureInstalled(*manifest, progress_cb, logger, install_policy); + if (!txn) { + return false; } - // Register with ORT + auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + #ifdef _WIN32 - // Prepend the EP directory to PATH for the process lifetime. - // WebGPU EP may delay-load additional dependencies from the same directory. - PrependDirToProcessPath(ep_dir); + // The provider delay-loads sibling DirectX compiler binaries after registration. + PrependDirToProcessPath(txn->bin_dir()); #endif - auto provider_path = ep_dir / kWebGpuProviderLib; - - logger.Log(LogLevel::Debug, - fmt::format("WebGPU EP: registering verified provider library {}", - provider_path.string())); - if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "WebGPU EP: ORT registration failed"); return false; @@ -332,15 +193,16 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, registered_ = true; + if (!txn->CommitActive(logger)) { + logger.Log(LogLevel::Warning, "WebGPU EP: failed to publish active bundle marker"); + } + if (progress_cb) { progress_cb(name_, 100.0f); } logger.Log(LogLevel::Information, - fmt::format("WebGPU EP: ready (install_path={})", ep_dir.string())); - - logger.Log(LogLevel::Debug, - fmt::format("WebGPU EP: success path complete for {}", ep_dir.string())); + fmt::format("WebGPU EP: ready (install_path={})", txn->bin_dir().string())); return true; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: error: {}", e.what())); @@ -348,4 +210,13 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, } } +bool WebGpuEpBootstrapper::IsSupportedPlatform() { +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__APPLE__) && defined(__aarch64__)) + return true; +#else + return false; +#endif +} + } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index e80d4651d..3fd2ed799 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -3,6 +3,7 @@ #pragma once #include "ep_detection/ep_bootstrapper.h" +#include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" #include @@ -13,16 +14,12 @@ class ILogger; /// Bootstrapper for the WebGPU execution provider. /// -/// Uses platform-specific package metadata (download URL and SHA-256 hash), -/// downloads the binary, verifies integrity, then registers with ORT. -/// -/// Supports Windows x64/ARM64, Linux x64, and macOS ARM64. +/// Installs and registers the WebGPU execution provider. class WebGpuEpBootstrapper : public IEpBootstrapper { public: - /// @param ep_dir Base directory for EP packages (e.g., appdata/foundry-local). - /// The WebGPU package will be at ep_dir/webgpu-ep/. + /// @param root_dir Root directory for the WebGPU EP bundle, e.g. "/ep/webgpu-ep". /// @param register_ep Callback to register the EP DLL with ORT. - WebGpuEpBootstrapper(std::string ep_dir, EpRegistrationCallback register_ep); + WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep); ~WebGpuEpBootstrapper() override = default; // Non-copyable @@ -35,12 +32,15 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { const ProgressCallback& progress_cb, ILogger& logger) override; + /// Whether Foundry Local publishes a WebGPU EP bundle for this platform. + static bool IsSupportedPlatform(); + private: - std::string ep_dir_; std::string name_ = "WebGpuExecutionProvider"; bool registered_ = false; int attempts_ = 0; EpRegistrationCallback register_ep_; + EpBundleInstaller installer_; }; } // namespace fl diff --git a/sdk_v2/cpp/src/http/http_download.cc b/sdk_v2/cpp/src/http/http_download.cc index 7354235dc..772893a58 100644 --- a/sdk_v2/cpp/src/http/http_download.cc +++ b/sdk_v2/cpp/src/http/http_download.cc @@ -22,12 +22,22 @@ namespace fl { +namespace { + +std::string RedactUrlForLog(const std::string& url) { + const auto query = url.find('?'); + return query == std::string::npos ? url : url.substr(0, query) + "?"; +} + +} // namespace + bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, std::atomic* cancel_flag, std::function progress_cb, - ILogger& logger) { + ILogger& logger, + int64_t max_bytes) { using namespace Azure::Core; using namespace Azure::Core::Http; @@ -50,26 +60,27 @@ bool HttpDownloadFile(const std::string& url, Azure::DateTime(std::chrono::system_clock::now() + std::chrono::minutes(30))); std::unique_ptr response; + const auto log_url = RedactUrlForLog(url); try { response = transport.Send(request, context); } catch (const std::exception& ex) { - logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", url, ": ", ex.what())); + logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", log_url, ": ", ex.what())); return false; } catch (...) { - logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", url, ": unknown exception")); + logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", log_url, ": unknown exception")); return false; } auto status = static_cast(response->GetStatusCode()); if (status < 200 || status >= 300) { - logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", url, ": HTTP status ", status)); + logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", log_url, ": HTTP status ", status)); return false; } auto body_stream = response->ExtractBodyStream(); if (!body_stream) { - logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", url, ": no body stream in response")); + logger.Log(LogLevel::Warning, MakeString("HTTP download failed for ", log_url, ": no body stream in response")); return false; } @@ -84,17 +95,24 @@ bool HttpDownloadFile(const std::string& url, content_length = std::stoll(cl_header->second); } catch (const std::exception& ex) { logger.Log(LogLevel::Warning, - MakeString("HTTP download: invalid Content-Length header for ", url, + MakeString("HTTP download: invalid Content-Length header for ", log_url, " (\"", cl_header->second, "\"): ", ex.what())); return false; } if (content_length < 0) { logger.Log(LogLevel::Warning, - MakeString("HTTP download: negative Content-Length for ", url, + MakeString("HTTP download: negative Content-Length for ", log_url, " (\"", cl_header->second, "\")")); return false; } + + if (max_bytes >= 0 && content_length > max_bytes) { + logger.Log(LogLevel::Warning, + MakeString("HTTP download: Content-Length ", content_length, " for ", log_url, + " exceeds the ", max_bytes, "-byte cap; refusing before reading any body")); + return false; + } } std::ofstream out(destination, std::ios::binary); @@ -108,41 +126,70 @@ bool HttpDownloadFile(const std::string& url, uint8_t buffer[kBufferSize]; int64_t bytes_downloaded = 0; int chunks_since_progress = 0; + auto remove_destination = [&]() { + std::error_code ec; + std::filesystem::remove(destination, ec); + }; - while (true) { - if (cancel_flag && cancel_flag->load()) { - out.close(); - std::filesystem::remove(destination); - return false; - } - - size_t bytes_read = body_stream->Read(buffer, kBufferSize, context); - if (bytes_read == 0) { - break; - } - - out.write(reinterpret_cast(buffer), static_cast(bytes_read)); - bytes_downloaded += static_cast(bytes_read); - - // Report progress every 32 chunks (~2MB), matching C# behavior - chunks_since_progress++; - if (progress_cb && content_length > 0 && chunks_since_progress >= 32) { - float percent = static_cast(bytes_downloaded * 100.0 / content_length); - progress_cb(percent); - chunks_since_progress = 0; + try { + while (true) { + if (cancel_flag && cancel_flag->load()) { + out.close(); + remove_destination(); + return false; + } + + size_t bytes_read = body_stream->Read(buffer, kBufferSize, context); + if (bytes_read == 0) { + break; + } + + out.write(reinterpret_cast(buffer), static_cast(bytes_read)); + if (!out) { + logger.Log(LogLevel::Warning, + MakeString("HTTP download: failed writing ", destination.string())); + out.close(); + remove_destination(); + return false; + } + bytes_downloaded += static_cast(bytes_read); + + if (max_bytes >= 0 && bytes_downloaded > max_bytes) { + logger.Log(LogLevel::Warning, + MakeString("HTTP download: body for ", log_url, " exceeded the ", max_bytes, "-byte cap")); + out.close(); + remove_destination(); + return false; + } + + chunks_since_progress++; + if (progress_cb && content_length > 0 && chunks_since_progress >= 32) { + float percent = static_cast(bytes_downloaded * 100.0 / content_length); + progress_cb(percent); + chunks_since_progress = 0; + } } + } catch (const std::exception& ex) { + logger.Log(LogLevel::Warning, MakeString("HTTP download failed while reading ", log_url, ": ", ex.what())); + out.close(); + remove_destination(); + return false; } out.close(); + if (!out) { + logger.Log(LogLevel::Warning, MakeString("HTTP download: failed finalizing ", destination.string())); + remove_destination(); + return false; + } // detect truncated transfer. If the server promised a content length and // we received fewer bytes, surface the error rather than reporting success. if (content_length > 0 && bytes_downloaded < content_length) { logger.Log(LogLevel::Warning, - MakeString("HTTP download truncated for ", url, ": got ", + MakeString("HTTP download truncated for ", log_url, ": got ", bytes_downloaded, " of ", content_length, " bytes")); - std::error_code ec; - std::filesystem::remove(destination, ec); + remove_destination(); return false; } diff --git a/sdk_v2/cpp/src/http/http_download.h b/sdk_v2/cpp/src/http/http_download.h index 2047544a9..a5e2835ab 100644 --- a/sdk_v2/cpp/src/http/http_download.h +++ b/sdk_v2/cpp/src/http/http_download.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include #include @@ -12,19 +13,23 @@ namespace fl { class ILogger; /// Download a file from an HTTP(S) URL to a local path. -/// Supports progress reporting and cancellation. +/// Supports progress reporting, cancellation, and an optional size cap. /// @param url The URL to download from. /// @param destination Local file path to write to. /// @param user_agent HTTP User-Agent header. /// @param cancel_flag Set to true to cancel. nullptr if not needed. /// @param progress_cb Called with percent 0.0-100.0. Empty = no callback. /// @param logger Logger for diagnostic output on failure. +/// @param max_bytes Fail closed if a Content-Length header exceeds this, and abort mid-stream +/// if the body exceeds it regardless of what Content-Length promised (defends +/// against a missing/incorrect header on chunked transfers). -1 means no cap. /// @return true on success, false on failure. bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, std::atomic* cancel_flag, std::function progress_cb, - ILogger& logger); + ILogger& logger, + int64_t max_bytes = -1); } // namespace fl diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index bd827d7a2..19b61b9ee 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -244,9 +244,10 @@ Manager::Manager(const Configuration& config) // Discover bootstrappers from available EP sources std::vector> bootstrappers; - // Detected once and reused below for the Foundry CUDA bootstrapper. HasNvidiaGpu() - // shells out to nvidia-smi, so caching the result here avoids a second subprocess spawn. - const bool has_nvidia_gpu = CudaEpBootstrapper::HasNvidiaGpu(); + // Detected once and reused below for the WinML catalog skip-list and CUDA bootstrapper. + // Avoid probing NVML on platforms where Foundry Local does not publish a CUDA bundle. + const bool has_nvidia_gpu = + CudaEpBootstrapper::IsSupportedPlatform() && CudaEpBootstrapper::HasNvidiaGpu(); #if FOUNDRY_LOCAL_HAS_EP_CATALOG // WinML EPs — enumerate from the OS EP catalog (Windows 10 19H1+ reg-free runtime). @@ -259,20 +260,18 @@ Manager::Manager(const Configuration& config) } #endif - const auto cache_dir = std::filesystem::path(*config_.model_cache_dir).parent_path(); - - // CUDA EP — only if an NVIDIA GPU is detected + // CUDA EP — only if an NVIDIA GPU is detected. Rooted under app_data_dir (not the model cache + // parent) so the install survives a user pointing model_cache_dir somewhere ephemeral. if (has_nvidia_gpu) { - const auto cuda_ep_dir = cache_dir / "cuda-ep"; - bootstrappers.push_back(std::make_unique(cuda_ep_dir.string(), register_ep)); + const auto cuda_ep_root = std::filesystem::path(*config_.app_data_dir) / "ep" / "cuda-ep"; + bootstrappers.push_back(std::make_unique(cuda_ep_root.string(), register_ep)); } - // WebGPU EP — only on platforms that ship a WebGPU EP payload (Windows - // x64/ARM64, macOS ARM64). Not injected on Linux or Android. -#if defined(_WIN32) || defined(__APPLE__) - const auto webgpu_ep_dir = cache_dir / "webgpu-ep"; - bootstrappers.push_back(std::make_unique(webgpu_ep_dir.string(), register_ep)); -#endif + // WebGPU EP — only on exact architectures for which a bundle is published. + if (WebGpuEpBootstrapper::IsSupportedPlatform()) { + const auto webgpu_ep_root = std::filesystem::path(*config_.app_data_dir) / "ep" / "webgpu-ep"; + bootstrappers.push_back(std::make_unique(webgpu_ep_root.string(), register_ep)); + } ep_detector_ = std::make_unique(*ort_api_, *ort_env_, std::move(bootstrappers), *logger_); @@ -341,6 +340,10 @@ Manager::~Manager() { telemetry_.reset(); ep_detector_.reset(); + // GenAI owns process-global ORT state and the CUDA add-on handle. Tear it down after every + // GenAI model/session is gone, but before unregistering provider libraries from our OrtEnv. + OgaShutdown(); + // Unregister EPs we registered, then drop our OrtEnv refcount. Best-effort: // log failures but don't throw from a destructor. if (ort_api_ != nullptr && ort_env_ != nullptr) { diff --git a/sdk_v2/cpp/src/manager.h b/sdk_v2/cpp/src/manager.h index 4b5440db7..cbfce33f5 100644 --- a/sdk_v2/cpp/src/manager.h +++ b/sdk_v2/cpp/src/manager.h @@ -118,7 +118,7 @@ class Manager { // ort_api_, ort_env_, // registered_ep_libraries_ — ORT environment & EP registrations; // released manually in ~Manager() after all - // consumers (sessions, ep_detector_) are gone. + // consumers and GenAI globals are gone. // logger_ — everything logs through this, destroyed last // ep_detector_ — detects HW acceleration; holds OrtEnv& (must // outlive ort_env_ release in ~Manager()) diff --git a/sdk_v2/cpp/src/util/zip_extract.cc b/sdk_v2/cpp/src/util/zip_extract.cc index 14514cc76..f432391f1 100644 --- a/sdk_v2/cpp/src/util/zip_extract.cc +++ b/sdk_v2/cpp/src/util/zip_extract.cc @@ -4,28 +4,21 @@ #include "logger.h" +#include +#include #include +#include #include +#include #include #include -#include +#include #include #include +#include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#else -#include -#include -#include -#include - -extern char** environ; // POSIX global — must be declared outside any namespace -#endif - namespace fl { bool IsSafeArchiveEntry(std::string_view entry) { @@ -33,42 +26,20 @@ bool IsSafeArchiveEntry(std::string_view entry) { return true; } - // Reject absolute POSIX paths. - if (entry.front() == '/') { - return false; - } - - // Reject leading backslash (Windows root-relative). - if (entry.front() == '\\') { - return false; - } - - // Reject Windows drive-letter prefix ("X:..."). Defensive: also reject any - // ':' that isn't in the drive-letter position, since archive entries should - // never contain ':'. - for (size_t i = 0; i < entry.size(); ++i) { - if (entry[i] != ':') { - continue; - } - - // Allow exactly the drive-letter form at position 1 (e.g. "C:") — but we - // still reject the entry overall because it implies an absolute path. + if (entry.front() == '/' || entry.front() == '\\' || + entry.find(':') != std::string_view::npos) { return false; } - // Split on both '/' and '\\' and reject any literal ".." component. size_t start = 0; for (size_t i = 0; i <= entry.size(); ++i) { - bool is_sep = (i == entry.size()) || entry[i] == '/' || entry[i] == '\\'; - if (!is_sep) { + if (i != entry.size() && entry[i] != '/' && entry[i] != '\\') { continue; } - auto component = entry.substr(start, i - start); - if (component == "..") { + if (entry.substr(start, i - start) == "..") { return false; } - start = i + 1; } @@ -77,281 +48,320 @@ bool IsSafeArchiveEntry(std::string_view entry) { namespace { -#ifdef _WIN32 - -/// Run `tar -tf ` and return the captured stdout. Returns false on -/// failure to spawn or on non-zero exit. Bypasses the shell to avoid injection. -/// On failure, emits a diagnostic via `logger`. -bool RunTarList(const std::filesystem::path& zip_path, std::string& out_stdout, ILogger& logger) { - // Create an anonymous pipe; child inherits the write end as stdout. - SECURITY_ATTRIBUTES sa{}; - sa.nLength = sizeof(sa); - sa.bInheritHandle = TRUE; - - HANDLE read_h = nullptr; - HANDLE write_h = nullptr; - if (!CreatePipe(&read_h, &write_h, &sa, 0)) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: CreatePipe failed (GetLastError={})", GetLastError())); - return false; +struct ArchiveDeleter { + void operator()(archive* value) const { + if (value) { + archive_read_free(value); + } } +}; - // The read end must NOT be inherited by the child. - SetHandleInformation(read_h, HANDLE_FLAG_INHERIT, 0); +using ArchivePtr = std::unique_ptr; - std::wstring command_line = L"tar -tf \"" + zip_path.wstring() + L"\""; - std::vector cmd_buf(command_line.begin(), command_line.end()); - cmd_buf.push_back(L'\0'); +enum class EntryKind { Regular, + Directory }; - STARTUPINFOW si{}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; - si.hStdOutput = write_h; - si.hStdError = write_h; - si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); - PROCESS_INFORMATION pi{}; +uint16_t ReadU16(const uint8_t* data) { + return static_cast(data[0] | (data[1] << 8)); +} - BOOL ok = CreateProcessW(nullptr, cmd_buf.data(), nullptr, nullptr, TRUE, - CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi); +uint32_t ReadU32(const uint8_t* data) { + return static_cast(data[0]) | (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | (static_cast(data[3]) << 24); +} - // Close the write end in the parent so ReadFile returns EOF when the child exits. - CloseHandle(write_h); +bool ValidateZipStructure(const std::filesystem::path& zip_path, ILogger& logger) { + constexpr uint32_t eocd_signature = 0x06054b50; + constexpr uint32_t central_directory_signature = 0x02014b50; + constexpr uint64_t eocd_size = 22; + constexpr uint64_t max_comment_size = 65535; - if (!ok) { - DWORD err = GetLastError(); - CloseHandle(read_h); - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: failed to spawn `tar -tf` for '{}' (GetLastError={})", - zip_path.string(), err)); + std::error_code ec; + const auto file_size = std::filesystem::file_size(zip_path, ec); + if (ec || file_size < eocd_size) { return false; } - // Drain the pipe into out_stdout. - std::string buf; - char chunk[4096]; - DWORD bytes_read = 0; - while (ReadFile(read_h, chunk, sizeof(chunk), &bytes_read, nullptr) && bytes_read > 0) { - buf.append(chunk, bytes_read); + std::ifstream input(zip_path, std::ios::binary); + const auto tail_size = std::min(file_size, eocd_size + max_comment_size); + std::vector tail(static_cast(tail_size)); + input.seekg(static_cast(file_size - tail_size)); + input.read(reinterpret_cast(tail.data()), static_cast(tail.size())); + if (!input) { + return false; } - CloseHandle(read_h); - WaitForSingleObject(pi.hProcess, INFINITE); - DWORD exit_code = 1; - GetExitCodeProcess(pi.hProcess, &exit_code); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); + size_t eocd_offset = std::string::npos; + for (size_t i = tail.size() - static_cast(eocd_size) + 1; i-- > 0;) { + if (ReadU32(tail.data() + i) == eocd_signature) { + const auto comment_size = ReadU16(tail.data() + i + 20); + if (i + eocd_size + comment_size == tail.size()) { + eocd_offset = i; + break; + } + } + } - if (exit_code != 0) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: `tar -tf` for '{}' exited {} \u2014 output: {}", - zip_path.string(), exit_code, buf)); + if (eocd_offset == std::string::npos) { + logger.Log(LogLevel::Warning, "ExtractZip: invalid end-of-central-directory record"); return false; } - out_stdout = std::move(buf); + const auto* eocd = tail.data() + eocd_offset; + const auto disk = ReadU16(eocd + 4); + const auto central_disk = ReadU16(eocd + 6); + const auto entries_on_disk = ReadU16(eocd + 8); + const auto total_entries = ReadU16(eocd + 10); + const uint64_t central_size = ReadU32(eocd + 12); + const uint64_t central_offset = ReadU32(eocd + 16); + const uint64_t absolute_eocd_offset = file_size - tail_size + eocd_offset; + + if (disk != 0 || central_disk != 0 || entries_on_disk != total_entries || total_entries == 0xffff || + central_size == 0xffffffff || central_offset == 0xffffffff || + central_offset > absolute_eocd_offset || central_size != absolute_eocd_offset - central_offset) { + logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory bounds"); + return false; + } + + if (total_entries > 0) { + uint8_t signature[4]; + input.clear(); + input.seekg(static_cast(central_offset)); + input.read(reinterpret_cast(signature), sizeof(signature)); + if (!input || ReadU32(signature) != central_directory_signature) { + logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory signature"); + return false; + } + } + return true; } -#else +std::string NormalizeEntryName(std::string_view name) { + std::string normalized(name); + std::replace(normalized.begin(), normalized.end(), '\\', '/'); + while (!normalized.empty() && normalized.back() == '/') { + normalized.pop_back(); + } + return std::filesystem::path(normalized).lexically_normal().generic_string(); +} -bool RunTarList(const std::filesystem::path& zip_path, std::string& out_stdout, ILogger& logger) { - int pipe_fds[2]; - if (pipe(pipe_fds) != 0) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: pipe() failed (errno={})", errno)); - return false; +std::string ComparisonKey(std::string value) { +#ifdef _WIN32 + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); +#endif + return value; +} + +ArchivePtr OpenArchive(const std::filesystem::path& zip_path, ILogger& logger) { + ArchivePtr reader(archive_read_new()); + if (!reader) { + logger.Log(LogLevel::Warning, "ExtractZip: failed to create archive reader"); + return nullptr; } - posix_spawn_file_actions_t actions; - posix_spawn_file_actions_init(&actions); - // Child: close the read end; route stdout/stderr to the write end; close write fd after dup. - posix_spawn_file_actions_addclose(&actions, pipe_fds[0]); - posix_spawn_file_actions_adddup2(&actions, pipe_fds[1], STDOUT_FILENO); - posix_spawn_file_actions_adddup2(&actions, pipe_fds[1], STDERR_FILENO); - posix_spawn_file_actions_addclose(&actions, pipe_fds[1]); - - std::string zip_str = zip_path.string(); - std::vector argv; - std::string arg_tar = "tar"; - std::string arg_tf = "-tf"; - argv.push_back(arg_tar.data()); - argv.push_back(arg_tf.data()); - argv.push_back(zip_str.data()); - argv.push_back(nullptr); - - pid_t pid; - int spawn_result = posix_spawnp(&pid, "tar", &actions, nullptr, argv.data(), environ); - posix_spawn_file_actions_destroy(&actions); - - // Parent closes the write end so read returns EOF when child exits. - close(pipe_fds[1]); - - if (spawn_result != 0) { - close(pipe_fds[0]); + archive_read_support_format_zip(reader.get()); +#ifdef _WIN32 + const auto open_result = archive_read_open_filename_w(reader.get(), zip_path.c_str(), 64 * 1024); +#else + const auto open_result = archive_read_open_filename(reader.get(), zip_path.c_str(), 64 * 1024); +#endif + if (open_result != ARCHIVE_OK) { logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: posix_spawnp('tar') failed for '{}' (errno={})", - zip_str, spawn_result)); - return false; + fmt::format("ExtractZip: failed to open '{}': {}", zip_path.string(), + archive_error_string(reader.get()))); + return nullptr; } - std::string buf; - char chunk[4096]; - ssize_t n = 0; - while ((n = read(pipe_fds[0], chunk, sizeof(chunk))) > 0) { - buf.append(chunk, static_cast(n)); - } - close(pipe_fds[0]); + return reader; +} - int status = 0; - waitpid(pid, &status, 0); - if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: `tar -tf` for '{}' exited abnormally (status={}) \u2014 output: {}", - zip_str, status, buf)); +bool ValidateArchive(const std::filesystem::path& zip_path, const ZipExtractLimits& limits, ILogger& logger) { + auto reader = OpenArchive(zip_path, logger); + if (!reader) { return false; } - out_stdout = std::move(buf); - return true; -} + std::unordered_map entries; + uint64_t total_size = 0; + size_t entry_count = 0; + archive_entry* entry = nullptr; -#endif + int result = ARCHIVE_OK; + while ((result = archive_read_next_header(reader.get(), &entry)) == ARCHIVE_OK) { + if (++entry_count > limits.max_entries) { + logger.Log(LogLevel::Warning, "ExtractZip: archive exceeds the entry-count limit"); + return false; + } -/// Validate every entry in `listing` (newline-separated). Returns true if all -/// entries are safe; on failure populates `bad_entry` with the offending name. -bool ValidateTarListing(std::string_view listing, std::string& bad_entry) { - size_t pos = 0; - while (pos < listing.size()) { - size_t nl = listing.find('\n', pos); - if (nl == std::string_view::npos) { - nl = listing.size(); + const char* raw_name = archive_entry_pathname_utf8(entry); + const std::string_view name = raw_name ? raw_name : ""; + if (!IsSafeArchiveEntry(name)) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: unsafe archive entry '{}'", name)); + return false; } - auto line = listing.substr(pos, nl - pos); + const auto normalized = NormalizeEntryName(name); + if (normalized.empty() || normalized == "." || !IsSafeArchiveEntry(normalized)) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: invalid archive entry '{}'", name)); + return false; + } - // Trim trailing CR for Windows tar output. - if (!line.empty() && line.back() == '\r') { - line.remove_suffix(1); + EntryKind kind; + const auto file_type = archive_entry_filetype(entry); + if (file_type == AE_IFREG) { + kind = EntryKind::Regular; + } else if (file_type == AE_IFDIR) { + kind = EntryKind::Directory; + } else { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: unsupported archive entry '{}'", name)); + return false; } - if (!line.empty() && !IsSafeArchiveEntry(line)) { - bad_entry.assign(line.data(), line.size()); + if (archive_entry_is_encrypted(entry) == 1) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: encrypted archive entry '{}'", name)); return false; } - pos = nl + 1; - } + const auto size = archive_entry_size(entry); + if (size < 0 || static_cast(size) > limits.max_entry_uncompressed_bytes) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: archive entry '{}' exceeds the size limit", name)); + return false; + } - return true; -} + total_size += static_cast(size); + if (total_size > limits.max_total_uncompressed_bytes) { + logger.Log(LogLevel::Warning, "ExtractZip: archive exceeds the total size limit"); + return false; + } -} // namespace + const auto key = ComparisonKey(normalized); + if (!entries.emplace(key, kind).second) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: duplicate archive entry '{}'", name)); + return false; + } + } -bool ExtractZip(const std::filesystem::path& zip_path, - const std::filesystem::path& destination, - ILogger& logger) { - if (!std::filesystem::exists(zip_path)) { + if (result != ARCHIVE_EOF) { logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: archive does not exist: '{}'", zip_path.string())); + fmt::format("ExtractZip: failed reading archive: {}", archive_error_string(reader.get()))); return false; } - std::filesystem::create_directories(destination); - - // Zip-slip defense: list the archive contents first and reject any entry - // whose path would escape the destination. Only proceed with extraction - // if every entry is safe. Since downloads are rare and Microsoft-controlled - // (EP runtime zips over HTTPS) the cost of an extra `tar -tf` is negligible. - std::string listing; - if (!RunTarList(zip_path, listing, logger)) { - return false; + for (const auto& [path, kind] : entries) { + auto parent = std::filesystem::path(path).parent_path(); + while (!parent.empty()) { + auto it = entries.find(ComparisonKey(parent.generic_string())); + if (it != entries.end() && it->second == EntryKind::Regular) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: file-directory collision at '{}'", path)); + return false; + } + parent = parent.parent_path(); + } } - std::string bad_entry; - if (!ValidateTarListing(listing, bad_entry)) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: refusing to extract '{}' \u2014 unsafe archive entry: '{}'", - zip_path.string(), bad_entry)); - // Refuse to extract. Caller logs a generic extraction-failed message; - // we deliberately do not throw to preserve the existing bool contract. + return true; +} + +bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem::path& destination, + const ZipExtractLimits& limits, ILogger& logger) { + auto reader = OpenArchive(zip_path, logger); + if (!reader) { return false; } - // Use system tar (available on Windows 10+ and all Linux distros) to avoid - // adding a minizip/libzip dependency. We bypass the shell to prevent injection. -#ifdef _WIN32 - std::wstring command_line = - L"tar -xf \"" + zip_path.wstring() + L"\" -C \"" + destination.wstring() + L"\""; + std::filesystem::create_directories(destination); + uint64_t total_written = 0; + archive_entry* entry = nullptr; + char buffer[64 * 1024]; + + int result = ARCHIVE_OK; + while ((result = archive_read_next_header(reader.get(), &entry)) == ARCHIVE_OK) { + const char* raw_name = archive_entry_pathname_utf8(entry); + if (!raw_name) { + logger.Log(LogLevel::Warning, "ExtractZip: archive entry has no path"); + return false; + } - // CreateProcessW requires a mutable command-line buffer - std::vector cmd_buf(command_line.begin(), command_line.end()); - cmd_buf.push_back(L'\0'); + const std::string name = NormalizeEntryName(raw_name); + const auto output_path = destination / name; - STARTUPINFOW si = {}; - si.cb = sizeof(si); - PROCESS_INFORMATION pi = {}; + if (archive_entry_filetype(entry) == AE_IFDIR) { + std::filesystem::create_directories(output_path); + continue; + } - if (!CreateProcessW(nullptr, cmd_buf.data(), nullptr, nullptr, FALSE, - CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: failed to spawn `tar -xf` for '{}' (GetLastError={})", - zip_path.string(), GetLastError())); - return false; - } + std::filesystem::create_directories(output_path.parent_path()); + std::ofstream output(output_path, std::ios::binary | std::ios::trunc); + if (!output) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: failed to create '{}'", output_path.string())); + return false; + } - WaitForSingleObject(pi.hProcess, INFINITE); + uint64_t entry_written = 0; + while (true) { + const auto count = archive_read_data(reader.get(), buffer, sizeof(buffer)); + if (count == 0) { + break; + } + if (count < 0) { + logger.Log(LogLevel::Warning, + fmt::format("ExtractZip: failed extracting '{}': {}", name, archive_error_string(reader.get()))); + return false; + } + + entry_written += static_cast(count); + total_written += static_cast(count); + if (entry_written > limits.max_entry_uncompressed_bytes || + total_written > limits.max_total_uncompressed_bytes) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: extracted size limit exceeded by '{}'", name)); + return false; + } + + output.write(buffer, count); + if (!output) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: failed writing '{}'", output_path.string())); + return false; + } + } + + output.close(); + if (!output) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: failed finalizing '{}'", output_path.string())); + return false; + } - DWORD exit_code = 1; - GetExitCodeProcess(pi.hProcess, &exit_code); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); + std::error_code ec; + std::filesystem::permissions(output_path, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | std::filesystem::perms::others_read, + std::filesystem::perm_options::replace, ec); + } - if (exit_code != 0) { + if (result != ARCHIVE_EOF) { logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: `tar -xf` for '{}' \u2192 '{}' exited {}", - zip_path.string(), destination.string(), exit_code)); + fmt::format("ExtractZip: failed reading archive: {}", archive_error_string(reader.get()))); return false; } return true; -#else - std::string zip_str = zip_path.string(); - std::string dest_str = destination.string(); - - // posix_spawnp searches PATH for "tar" - std::vector argv; - std::string arg_tar = "tar"; - std::string arg_xf = "-xf"; - std::string arg_c = "-C"; - argv.push_back(arg_tar.data()); - argv.push_back(arg_xf.data()); - argv.push_back(zip_str.data()); - argv.push_back(arg_c.data()); - argv.push_back(dest_str.data()); - argv.push_back(nullptr); - - pid_t pid; - int spawn_result = posix_spawnp(&pid, "tar", nullptr, nullptr, argv.data(), environ); - - if (spawn_result != 0) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: posix_spawnp('tar') failed for '{}' (errno={})", - zip_str, spawn_result)); - return false; - } +} - int status; - waitpid(pid, &status, 0); +} // namespace - if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: `tar -xf` for '{}' \u2192 '{}' exited abnormally (status={})", - zip_str, dest_str, status)); +bool ExtractZip(const std::filesystem::path& zip_path, + const std::filesystem::path& destination, + ILogger& logger, + const ZipExtractLimits& limits) { + std::error_code ec; + if (!std::filesystem::is_regular_file(zip_path, ec)) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: archive does not exist: '{}'", zip_path.string())); return false; } - return true; -#endif + return ValidateZipStructure(zip_path, logger) && ValidateArchive(zip_path, limits, logger) && + ExtractArchive(zip_path, destination, limits, logger); } } // namespace fl diff --git a/sdk_v2/cpp/src/util/zip_extract.h b/sdk_v2/cpp/src/util/zip_extract.h index ce4916c03..3a58b9de4 100644 --- a/sdk_v2/cpp/src/util/zip_extract.h +++ b/sdk_v2/cpp/src/util/zip_extract.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once +#include #include #include @@ -9,24 +10,42 @@ namespace fl { class ILogger; -/// Returns true if `entry` is a safe archive entry path — i.e. extracting it +/// Returns true if `entry` is a safe archive entry path, meaning extracting it /// will not escape the destination directory ("zip-slip" defense). Rejects: /// * any entry with a `..` path component (split on '/' or '\\') /// * absolute POSIX paths (starting with '/') /// * Windows absolute paths (drive-letter `X:` prefix or leading '\\') -/// Used as a pre-validation pass before invoking the system tar extractor. bool IsSafeArchiveEntry(std::string_view entry); -/// Extract a ZIP archive to a directory. +/// Resource bounds enforced by ExtractZip to defend against zip-bomb / resource-exhaustion +/// archives. Defaults are generous for legitimate EP packages (hundreds of small files, +/// each well under a gigabyte) while still rejecting pathological inputs. +struct ZipExtractLimits { + size_t max_entries = 20000; + uint64_t max_total_uncompressed_bytes = 8ULL * 1024 * 1024 * 1024; // 8 GiB + uint64_t max_entry_uncompressed_bytes = 4ULL * 1024 * 1024 * 1024; +}; + +/// Extract a ZIP archive to a directory using an in-process parser (no subprocess, no shell). /// Creates the destination directory if it doesn't exist. -/// Performs a zip-slip pre-validation pass over the archive's entry list and -/// refuses to extract if any entry would escape `destination`. -/// Diagnostic messages for any failure (spawn error, non-zero tar exit, -/// unsafe entry, etc.) are emitted via `logger` so production failures are +/// +/// Every central-directory entry is validated before any bytes are written: +/// * zip-slip defense (see IsSafeArchiveEntry) +/// * symlinks and special files (character/block devices, FIFOs, sockets) are rejected +/// * duplicate entry paths are rejected +/// * entry count and per-entry / total uncompressed size are bounded by `limits` +/// * only the STORE and DEFLATE compression methods are supported +/// Extraction only begins once every entry in the archive has passed validation; a single +/// unsafe or oversized entry fails the whole archive. Per-entry CRC-32 is checked against the +/// value recorded in the archive as an integrity check independent of any caller-side hash +/// verification performed on the extracted files. +/// +/// Diagnostic messages for any failure are emitted via `logger` so production failures are /// debuggable from the SDK log. /// @return true on success. bool ExtractZip(const std::filesystem::path& zip_path, const std::filesystem::path& destination, - ILogger& logger); + ILogger& logger, + const ZipExtractLimits& limits = {}); } // namespace fl diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index 214594599..8443a1d4c 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -23,9 +23,11 @@ add_executable(foundry_local_tests internal_api/chat_completions_converter_test.cc internal_api/configuration_test.cc internal_api/cross_process_file_lock_test.cc + internal_api/cuda_ep_bootstrapper_test.cc internal_api/download_test.cc internal_api/embeddings/contracts_embeddings_test.cc internal_api/embeddings/fp16_test.cc + internal_api/ep_bundle_installer_test.cc internal_api/ep_detector_test.cc internal_api/exception_test.cc internal_api/execution_provider_test.cc @@ -42,6 +44,7 @@ add_executable(foundry_local_tests internal_api/model_io_info_test.cc internal_api/model_load_manager_test.cc internal_api/model_sorting_test.cc + internal_api/nvml_gpu_detector_test.cc internal_api/platform_path_test.cc internal_api/response_converter_test.cc internal_api/response_store_test.cc @@ -59,6 +62,7 @@ add_executable(foundry_local_tests internal_api/toolcalling/grammar_test.cc internal_api/utils_test.cc internal_api/web_service_test.cc + internal_api/webgpu_ep_bootstrapper_test.cc internal_api/winml_provider_allowlist_test.cc internal_api/zip_extract_test.cc ) diff --git a/sdk_v2/cpp/test/internal_api/c_api_test.cc b/sdk_v2/cpp/test/internal_api/c_api_test.cc index 993b9ac99..5fc46d302 100644 --- a/sdk_v2/cpp/test/internal_api/c_api_test.cc +++ b/sdk_v2/cpp/test/internal_api/c_api_test.cc @@ -170,6 +170,23 @@ TEST(CApiTest, ManagerCreateAndRelease) { api->Manager_Release(mgr); } +TEST(CApiTest, ManagerCanBeRecreatedAfterRelease) { + const flApi* api = GetApi(); + ASSERT_NE(api, nullptr); + + for (int i = 0; i < 2; ++i) { + flConfiguration* config = CreateTestConfig(api); + ASSERT_NE(config, nullptr); + + flManager* mgr = nullptr; + ASSERT_FL_OK(api, api->Manager_Create(config, &mgr)); + ASSERT_NE(mgr, nullptr); + + api->GetConfigurationApi()->Configuration_Release(config); + api->Manager_Release(mgr); + } +} + TEST(CApiTest, ManagerReleaseNullIsNoOp) { const flApi* api = GetApi(); ASSERT_NE(api, nullptr); diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc new file mode 100644 index 000000000..bc295e881 --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/cuda_ep_bootstrapper.h" + +#include + +namespace fl { + +TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) + EXPECT_TRUE(CudaEpBootstrapper::IsSupportedPlatform()); +#else + EXPECT_FALSE(CudaEpBootstrapper::IsSupportedPlatform()); +#endif +} + +} // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc new file mode 100644 index 000000000..2f47c80e5 --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -0,0 +1,626 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/ep_bundle_installer.h" + +#include "logger.h" +#include "util/file_lock.h" +#include "util/sha256.h" + +#include "utils/temp_path.h" +#include "utils/zip_builder.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fl { + +namespace { + +class NullLogger : public ILogger { + public: + void Log(LogLevel /*level*/, std::string_view /*message*/) override {} +}; + +std::vector AsBytes(const std::string& text) { + return std::vector(text.begin(), text.end()); +} + +std::string HashOf(const std::vector& bytes) { + auto tmp = test::TempPath::CreateTempFile("fl_bundle_installer_hash_"); + std::ofstream out(tmp.path(), std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + return Sha256File(tmp.path()); +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +class FakeDownloads { + public: + void SetSequence(const std::string& url, std::vector> payloads) { + payloads_[url] = std::move(payloads); + } + + int CallCount(const std::string& url) const { + auto it = call_counts_.find(url); + return it == call_counts_.end() ? 0 : it->second; + } + + EpArtifactDownloadFn AsFn() { + return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, + std::atomic* cancel_flag, const std::function& progress_cb, + ILogger& /*logger*/) -> bool { + auto it = payloads_.find(url); + if (it == payloads_.end() || it->second.empty()) { + return false; + } + + if (progress_cb) { + progress_cb(0.0f); + } + if (cancel_flag && cancel_flag->load()) { + return false; + } + + int& count = call_counts_[url]; + size_t index = std::min(static_cast(count), it->second.size() - 1); + const auto& bytes = it->second[index]; + count++; + + std::filesystem::create_directories(destination.parent_path()); + std::ofstream out(destination, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + + if (progress_cb) { + progress_cb(100.0f); + } + return true; + }; + } + + private: + std::map>> payloads_; + std::map call_counts_; +}; + +EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url, + const std::string& sha256) { + EpBundleManifest manifest; + manifest.bundle_id = bundle_id; + manifest.provider_relative_path = "provider.so"; + manifest.artifacts = {EpBundleArtifact{.id = "provider", + .url = url, + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "provider.so", + .raw_sha256 = sha256, + .raw_max_bytes = 1024}}; + return manifest; +} + +EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::string& url, + const std::string& archive_sha256, + std::vector extracted_files) { + EpBundleManifest manifest; + manifest.bundle_id = bundle_id; + manifest.provider_relative_path = "provider.dll"; + manifest.artifacts = {EpBundleArtifact{.id = "archive", + .url = url, + .is_archive = true, + .archive_sha256 = archive_sha256, + .extracted_files = std::move(extracted_files), + .archive_max_bytes = 1024 * 1024, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0}}; + return manifest; +} + +std::optional InstallAndCommit(EpBundleInstaller& installer, + const EpBundleManifest& manifest, ILogger& logger) { + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + if (!txn) { + return std::nullopt; + } + + const auto bin_dir = txn->bin_dir(); + if (!txn->CommitActive(logger)) { + return std::nullopt; + } + + return bin_dir; +} + +} // namespace + +TEST(EpBundleInstallerTest, UnsupportedManifestFailsClosedWithoutDownloading) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + FakeDownloads downloads; + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + + EpBundleManifest manifest; + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); + EXPECT_EQ(downloads.CallCount(""), 0); +} + +TEST(EpBundleInstallerTest, InstallsRawArtifactAndVerifiesContent) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("provider-binary-contents"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + EXPECT_EQ(ReadFile(txn->bin_dir() / "provider.so"), "provider-binary-contents"); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1); +} + +TEST(EpBundleInstallerTest, ReusesValidBundleWithoutRedownloading) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("provider-binary-contents"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + std::filesystem::path first_bin; + { + auto first = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(first, nullptr); + first_bin = first->bin_dir(); + ASSERT_TRUE(first->CommitActive(logger)); + } + + auto second = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(second, nullptr); + EXPECT_EQ(first_bin, second->bin_dir()); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1) + << "reusing an already-verified bundle must not re-download"; +} + +TEST(EpBundleInstallerTest, ForceDownloadRedownloadsEveryArtifactFromValidActiveBundle) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto first = AsBytes("first"); + const auto second = AsBytes("second"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/first.bin", {first}); + downloads.SetSequence("https://example.test/second.bin", {second}); + + EpBundleManifest manifest; + manifest.bundle_id = "bundle"; + manifest.provider_relative_path = "first.bin"; + manifest.artifacts = { + EpBundleArtifact{.id = "first", + .url = "https://example.test/first.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "first.bin", + .raw_sha256 = HashOf(first), + .raw_max_bytes = 1024}, + EpBundleArtifact{.id = "second", + .url = "https://example.test/second.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "second.bin", + .raw_sha256 = HashOf(second), + .raw_max_bytes = 1024}, + }; + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + + auto replacement = installer.EnsureInstalled( + manifest, /*progress_cb=*/nullptr, logger, EpBundleInstallPolicy::ForceDownload); + + ASSERT_NE(replacement, nullptr); + EXPECT_EQ(ReadFile(replacement->bin_dir() / "first.bin"), "first"); + EXPECT_EQ(ReadFile(replacement->bin_dir() / "second.bin"), "second"); + EXPECT_EQ(downloads.CallCount("https://example.test/first.bin"), 2); + EXPECT_EQ(downloads.CallCount("https://example.test/second.bin"), 2); +} + +TEST(EpBundleInstallerTest, RawHashMismatchFailsWithoutRetry) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto wrong_payload = AsBytes("this-is-not-what-you-expected"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {wrong_payload}); + + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(AsBytes("expected"))); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1) + << "raw artifacts get zero retries on a hash mismatch"; +} + +TEST(EpBundleInstallerTest, ArchiveHashMismatchRetriesOnceThenSucceeds) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("dll-bytes")); + auto good_archive = builder.Build(); + auto bad_archive = AsBytes("not-a-real-zip-at-all"); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {bad_archive, good_archive}); + + auto manifest = MakeArchiveManifest( + "bundle-1", "https://example.test/archive.zip", HashOf(good_archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + EXPECT_EQ(ReadFile(txn->bin_dir() / "provider.dll"), "dll-bytes"); + EXPECT_EQ(downloads.CallCount("https://example.test/archive.zip"), 2); +} + +TEST(EpBundleInstallerTest, ArchiveHashMismatchTwiceFails) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto bad_archive = AsBytes("still-not-a-zip"); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {bad_archive}); + + auto manifest = + MakeArchiveManifest( + "bundle-1", "https://example.test/archive.zip", HashOf(AsBytes("expected-archive")), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/archive.zip"), 2) + << "archives get exactly one fresh retry on a hash mismatch"; +} + +TEST(EpBundleInstallerTest, ExtractedFileMismatchFailsWithoutRetryEvenThoughArchiveHashMatched) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("actual-content")); + auto archive = builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {archive}); + + auto manifest = MakeArchiveManifest( + "bundle-1", "https://example.test/archive.zip", HashOf(archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("different-content"))}}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/archive.zip"), 1) + << "extracted-member mismatches are never retried within the same call"; +} + +TEST(EpBundleInstallerTest, MissingExpectedExtractedFileFails) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("actual-content")); + auto archive = builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {archive}); + + auto manifest = MakeArchiveManifest( + "bundle-1", "https://example.test/archive.zip", HashOf(archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("actual-content"))}, + EpBundleFile{.relative_path = "missing.dll", .sha256 = HashOf(AsBytes("whatever"))}}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); +} + +TEST(EpBundleInstallerTest, CommitActiveWritesActiveMarkerFile) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + + auto manifest = MakeRawManifest("bundle-42", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + EXPECT_TRUE(txn->CommitActive(logger)); + + EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-42-")); +} + +TEST(EpBundleInstallerTest, CommitActiveReplacesExistingActiveMarker) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + std::filesystem::create_directories(root.path()); + { + std::ofstream(root.path() / "active") << "stale-previous-generation"; + } + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + EXPECT_TRUE(txn->CommitActive(logger)); + EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-1-")) + << "publishing replaces an existing active marker atomically"; +} + +TEST(EpBundleInstallerTest, InstallTransactionHoldsLockUntilReleased) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto lock_path = root.path() / "test.lock"; + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + + EXPECT_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }, std::runtime_error); + + txn.reset(); + + EXPECT_NO_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }); +} + +TEST(EpBundleInstallerTest, CommitActiveRevalidatesUnderLockAndRefusesTamperedBundle) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + ASSERT_TRUE(active_v1.starts_with("bundle-v1-")); + + auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + { + std::ofstream(txn->bin_dir() / "unexpected.txt") << "surprise"; + } + + EXPECT_FALSE(txn->CommitActive(logger)) << "re-verification under the lock must reject a tampered bundle"; + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1) + << "a bundle failing re-verification must not advance the active marker"; + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) + << "the previous generation is preserved when activation fails"; +} + +TEST(EpBundleInstallerTest, CommitActivePreservesPreviousMarkerWhenPublicationFails) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + ASSERT_TRUE(active_v1.starts_with("bundle-v1-")); + + auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + + std::filesystem::remove(root.path() / "active"); + std::filesystem::create_directory(root.path() / "active"); + { + std::ofstream(root.path() / "active" / "blocker") << "x"; + } + + EXPECT_FALSE(txn->CommitActive(logger)) << "a failed marker publication is reported as failure"; + EXPECT_TRUE(std::filesystem::is_directory(root.path() / "active")) + << "the failed publication left the marker path untouched"; + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) + << "the previous generation is preserved when publication fails"; + EXPECT_TRUE(std::filesystem::exists(txn->bin_dir())); +} + +TEST(EpBundleInstallerTest, CommitActiveRemovesOldGenerations) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + ASSERT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + + auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + ASSERT_TRUE(InstallAndCommit(installer, manifest_v2, logger).has_value()); + const auto active_v2 = ReadFile(root.path() / "active"); + + EXPECT_FALSE(std::filesystem::exists(root.path() / "bundles" / active_v1)) + << "the previous generation is removed once the new one is committed"; + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); +} + +TEST(EpBundleInstallerTest, StaleStagingDirectoryIsCleanedUpOnNextInstall) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + std::filesystem::create_directories(root.path() / "staging" / "leftover-from-a-crash"); + + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + EXPECT_FALSE(std::filesystem::exists(root.path() / "staging" / "leftover-from-a-crash")); +} + +TEST(EpBundleInstallerTest, DoesNotCopyUnexpectedFilesFromExistingBundle) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + + auto bin_dir = root.path() / "bundles" / ReadFile(root.path() / "active") / "bin"; + { + std::ofstream(bin_dir / "unexpected.txt") << "surprise"; + } + + auto second = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(second, nullptr); + EXPECT_FALSE(std::filesystem::exists(second->bin_dir() / "unexpected.txt")); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1); +} + +TEST(EpBundleInstallerTest, FreshInstallRejectsArchiveWithUndeclaredExtraFile) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("dll-bytes")); + builder.AddEntry("extra.txt", AsBytes("undeclared-payload")); + auto archive = builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {archive}); + + auto manifest = MakeArchiveManifest( + "bundle-1", "https://example.test/archive.zip", HashOf(archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + EXPECT_EQ(txn, nullptr); + EXPECT_FALSE(std::filesystem::exists(root.path() / "active")) + << "a bundle that never published must never be activated either"; + + auto staging_dir = root.path() / "staging"; + if (std::filesystem::exists(staging_dir)) { + EXPECT_TRUE(std::filesystem::is_empty(staging_dir)); + } +} + +TEST(EpBundleInstallerTest, ReusesValidArtifactsAndDownloadsOnlyMismatches) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto first = AsBytes("first"); + const auto second = AsBytes("second"); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/first.bin", {first}); + downloads.SetSequence("https://example.test/second.bin", {second}); + + EpBundleManifest manifest; + manifest.bundle_id = "bundle"; + manifest.provider_relative_path = "first.bin"; + manifest.artifacts = { + EpBundleArtifact{.id = "first", + .url = "https://example.test/first.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "first.bin", + .raw_sha256 = HashOf(first), + .raw_max_bytes = 1024}, + EpBundleArtifact{.id = "second", + .url = "https://example.test/second.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .archive_max_bytes = 0, + .raw_relative_path = "second.bin", + .raw_sha256 = HashOf(second), + .raw_max_bytes = 1024}, + }; + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + + const auto active_bin = root.path() / "bundles" / ReadFile(root.path() / "active") / "bin"; + std::ofstream(active_bin / "second.bin", std::ios::binary | std::ios::trunc) << "corrupt"; + + downloads.SetSequence("https://example.test/second.bin", {second}); + auto replacement = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(replacement, nullptr); + EXPECT_EQ(ReadFile(replacement->bin_dir() / "first.bin"), "first"); + EXPECT_EQ(ReadFile(replacement->bin_dir() / "second.bin"), "second"); + EXPECT_EQ(downloads.CallCount("https://example.test/first.bin"), 1); + EXPECT_EQ(downloads.CallCount("https://example.test/second.bin"), 2); +} + +TEST(EpBundleInstallerTest, CancellationDuringDownloadFails) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + IEpBootstrapper::ProgressCallback cancel_immediately = [](const std::string&, float) { return false; }; + auto txn = installer.EnsureInstalled(manifest, cancel_immediately, logger); + EXPECT_EQ(txn, nullptr); +} + +} // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/nvml_gpu_detector_test.cc b/sdk_v2/cpp/test/internal_api/nvml_gpu_detector_test.cc new file mode 100644 index 000000000..244b41177 --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/nvml_gpu_detector_test.cc @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Unit tests for the pure compute-capability logic in ep_detection/nvml_gpu_detector. The NVML +// dynamic-loading path itself requires a real NVIDIA driver and is exercised only by +// EpDetectionApiTest integration tests, not here. +#include "ep_detection/nvml_gpu_detector.h" + +#include + +#include +#include + +namespace fl { + +TEST(HasQualifyingComputeCapabilityTest, EmptyListReturnsFalse) { + EXPECT_FALSE(HasQualifyingComputeCapability({})); +} + +TEST(HasQualifyingComputeCapabilityTest, ExactlyAtDefaultThresholdQualifies) { + EXPECT_TRUE(HasQualifyingComputeCapability({{5, 0}})); +} + +TEST(HasQualifyingComputeCapabilityTest, BelowDefaultThresholdDoesNotQualify) { + EXPECT_FALSE(HasQualifyingComputeCapability({{4, 9}})); + EXPECT_FALSE(HasQualifyingComputeCapability({{3, 5}})); +} + +TEST(HasQualifyingComputeCapabilityTest, AboveDefaultThresholdQualifies) { + EXPECT_TRUE(HasQualifyingComputeCapability({{7, 5}})); + EXPECT_TRUE(HasQualifyingComputeCapability({{9, 0}})); +} + +TEST(HasQualifyingComputeCapabilityTest, HigherMajorWithLowerMinorStillQualifies) { + // (6, 0) beats (5, 9) because major dominates minor. + EXPECT_TRUE(HasQualifyingComputeCapability({{6, 0}})); +} + +TEST(HasQualifyingComputeCapabilityTest, OneQualifyingDeviceAmongManyIsEnough) { + std::vector> capabilities = {{3, 0}, {4, 5}, {8, 6}}; + EXPECT_TRUE(HasQualifyingComputeCapability(capabilities)); +} + +TEST(HasQualifyingComputeCapabilityTest, NoDeviceQualifiesReturnsFalse) { + std::vector> capabilities = {{3, 0}, {4, 5}, {4, 9}}; + EXPECT_FALSE(HasQualifyingComputeCapability(capabilities)); +} + +TEST(HasQualifyingComputeCapabilityTest, CustomThresholdIsRespected) { + EXPECT_TRUE(HasQualifyingComputeCapability({{7, 0}}, /*min_major=*/7, /*min_minor=*/0)); + EXPECT_FALSE(HasQualifyingComputeCapability({{6, 9}}, /*min_major=*/7, /*min_minor=*/0)); +} + +TEST(HasQualifyingComputeCapabilityTest, CustomThresholdMinorBoundary) { + EXPECT_TRUE(HasQualifyingComputeCapability({{8, 6}}, /*min_major=*/8, /*min_minor=*/6)); + EXPECT_FALSE(HasQualifyingComputeCapability({{8, 5}}, /*min_major=*/8, /*min_minor=*/6)); +} + +} // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc new file mode 100644 index 000000000..798b2c5e3 --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/webgpu_ep_bootstrapper.h" + +#include "logger.h" +#include "utils/temp_path.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace fl { + +namespace { + +constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; + +void SetEnvValue(const char* name, const std::optional& value) { +#ifdef _WIN32 + _putenv_s(name, value.value_or("").c_str()); +#else + if (value.has_value()) { + setenv(name, value->c_str(), 1); + } else { + unsetenv(name); + } +#endif +} + +class ScopedEnvironmentVariable { + public: + ScopedEnvironmentVariable(const char* name, std::string value) + : name_(name) { + if (const auto* previous = std::getenv(name); previous != nullptr) { + previous_ = previous; + } + + SetEnvValue(name_, value); + } + + ~ScopedEnvironmentVariable() { + SetEnvValue(name_, previous_); + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& operator=(const ScopedEnvironmentVariable&) = delete; + + private: + const char* name_; + std::optional previous_; +}; + +} // namespace + +TEST(WebGpuEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__APPLE__) && defined(__aarch64__)) + EXPECT_TRUE(WebGpuEpBootstrapper::IsSupportedPlatform()); +#else + EXPECT_FALSE(WebGpuEpBootstrapper::IsSupportedPlatform()); +#endif +} + +TEST(WebGpuEpBootstrapperTest, OverrideRegistersUsingExistingProviderConvention) { + auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); + const auto provider_path = root.path() / "custom_webgpu_provider"; + std::ofstream(provider_path, std::ios::binary) << "test provider"; + ScopedEnvironmentVariable override(kOverrideEnv, provider_path.string()); + + std::string registered_name; + std::filesystem::path registered_path; + int registration_count = 0; + auto register_ep = [&](const std::string& name, const std::filesystem::path& path) { + registered_name = name; + registered_path = path; + ++registration_count; + return true; + }; + WebGpuEpBootstrapper bootstrapper(root.string(), register_ep); + StderrLogger logger; + std::vector> progress; + + EXPECT_TRUE(bootstrapper.DownloadAndRegister( + false, + [&](const std::string& name, float percent) { + progress.emplace_back(name, percent); + return true; + }, + logger)); + + EXPECT_TRUE(bootstrapper.IsRegistered()); + EXPECT_EQ(registered_name, "Foundry.WebGPU"); + EXPECT_EQ(registered_path, std::filesystem::absolute(provider_path)); + EXPECT_EQ(registration_count, 1); + ASSERT_EQ(progress.size(), 2u); + EXPECT_EQ(progress[0], std::make_pair(std::string("WebGpuExecutionProvider"), 90.0f)); + EXPECT_EQ(progress[1], std::make_pair(std::string("WebGpuExecutionProvider"), 100.0f)); + + progress.clear(); + EXPECT_TRUE(bootstrapper.DownloadAndRegister( + false, + [&](const std::string& name, float percent) { + progress.emplace_back(name, percent); + return true; + }, + logger)); + + EXPECT_EQ(registration_count, 1); + ASSERT_EQ(progress.size(), 1u); + EXPECT_EQ(progress[0], std::make_pair(std::string("WebGpuExecutionProvider"), 100.0f)); +} + +} // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc index 35d0c8965..5fd61d2d0 100644 --- a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc +++ b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc @@ -1,10 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// Unit tests for zip-slip pre-validation in util/zip_extract. +// Unit tests for util/zip_extract: zip-slip pre-validation plus the bounded in-process extractor +// (STORE + DEFLATE, symlink/special/duplicate rejection, and resource limits). All archives are +// built in-process — no dependency on an external zip/tar tool. #include "util/zip_extract.h" +#include "logger.h" + +#include "utils/temp_path.h" +#include "utils/zip_builder.h" + #include +#include +#include +#include +#include + namespace fl { TEST(IsSafeArchiveEntryTest, AcceptsSimpleFilename) { @@ -76,4 +88,310 @@ TEST(IsSafeArchiveEntryTest, AcceptsParentLikeFilename) { EXPECT_TRUE(IsSafeArchiveEntry("dir/...hidden")); } +// ======================================================================== +// ExtractZip — bounded in-process extractor +// ======================================================================== + +namespace { + +class NullLogger : public ILogger { + public: + void Log(LogLevel /*level*/, std::string_view /*message*/) override {} +}; + +std::vector ReadFileBytes(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return std::vector((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +std::vector AsBytes(const std::string& text) { + return std::vector(text.begin(), text.end()); +} + +} // namespace + +using test::ZipBuilder; + +TEST(ExtractZipTest, ExtractsStoredEntry) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello world")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ASSERT_TRUE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_EQ(ReadFileBytes(dest.path() / "hello.txt"), AsBytes("hello world")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, ExtractsDeflatedEntry) { + std::string content(4096, 'A'); // large + repetitive so DEFLATE actually compresses it + ZipBuilder builder; + builder.AddEntry("big.bin", AsBytes(content), /*compress=*/true); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ASSERT_TRUE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_EQ(ReadFileBytes(dest.path() / "big.bin"), AsBytes(content)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, ExtractsNestedDirectoriesAndMultipleEntries) { + ZipBuilder builder; + builder.AddDirectory("sub/"); + builder.AddEntry("sub/a.txt", AsBytes("a")); + builder.AddEntry("b.txt", AsBytes("b"), /*compress=*/true); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ASSERT_TRUE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_EQ(ReadFileBytes(dest.path() / "sub" / "a.txt"), AsBytes("a")); + EXPECT_EQ(ReadFileBytes(dest.path() / "b.txt"), AsBytes("b")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsPathTraversalEntry) { + ZipBuilder builder; + builder.AddEntry("../escape.txt", AsBytes("evil")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_FALSE(std::filesystem::exists(dest.path() / ".." / "escape.txt")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsSymlinkEntry) { + constexpr uint32_t kSymlinkMode = 0xA1FF; // S_IFLNK + ZipBuilder builder; + builder.AddEntry("link", AsBytes("/etc/passwd"), /*compress=*/false, kSymlinkMode); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsSpecialFileEntry) { + constexpr uint32_t kCharDeviceMode = 0x21FF; // S_IFCHR + ZipBuilder builder; + builder.AddEntry("dev-null", AsBytes(""), /*compress=*/false, kCharDeviceMode); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsDuplicateEntries) { + ZipBuilder builder; + builder.AddEntry("dup.txt", AsBytes("first")); + builder.AddEntry("dup.txt", AsBytes("second")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsEntryCountOverLimit) { + ZipBuilder builder; + builder.AddEntry("a.txt", AsBytes("a")); + builder.AddEntry("b.txt", AsBytes("b")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ZipExtractLimits limits; + limits.max_entries = 1; + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger, limits)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsEntryExceedingPerEntrySizeLimit) { + ZipBuilder builder; + builder.AddEntry("big.bin", AsBytes(std::string(1024, 'z'))); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ZipExtractLimits limits; + limits.max_entry_uncompressed_bytes = 100; + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger, limits)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsTotalSizeOverLimit) { + ZipBuilder builder; + builder.AddEntry("a.bin", AsBytes(std::string(600, 'a'))); + builder.AddEntry("b.bin", AsBytes(std::string(600, 'b'))); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + ZipExtractLimits limits; + limits.max_total_uncompressed_bytes = 1000; + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger, limits)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, FailsOnMissingArchive) { + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + EXPECT_FALSE(ExtractZip("/nonexistent/path/does-not-exist.zip", dest.path(), logger)); +} + +TEST(ExtractZipTest, FailsOnTruncatedArchive) { + ZipBuilder builder; + builder.AddEntry("a.txt", AsBytes("a")); + auto full_bytes = builder.Build(); + auto path = test::MakeUniqueTempPath("fl_zip_extract_truncated_"); + { + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(full_bytes.data()), + static_cast(full_bytes.size() / 2)); + } + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(path, dest.path(), logger)); + std::filesystem::remove(path); +} + +namespace { + +// Patches a little-endian uint32 at `offset` within `bytes` in place — used to craft a +// malformed EOCD record from an otherwise well-formed archive built by ZipBuilder. +void PatchU32(std::vector& bytes, size_t offset, uint32_t value) { + bytes[offset] = static_cast(value & 0xFF); + bytes[offset + 1] = static_cast((value >> 8) & 0xFF); + bytes[offset + 2] = static_cast((value >> 16) & 0xFF); + bytes[offset + 3] = static_cast((value >> 24) & 0xFF); +} + +// EOCD (End Of Central Directory) is the trailing 22 bytes for an archive with no comment; +// cd_size lives at byte 12 of the record and cd_offset at byte 16 — see zip_extract.cc. +constexpr size_t kEocdRecordSize = 22; +constexpr size_t kEocdCdSizeOffset = 12; +constexpr size_t kEocdCdOffsetOffset = 16; + +std::filesystem::path WriteBytesToTempFile(const std::vector& bytes, std::string_view prefix) { + auto path = test::MakeUniqueTempPath(std::string(prefix)); + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + return path; +} + +// Central-directory record field offsets relative to a record's start (see zip_extract.cc): +// compressed_size at byte 20, uncompressed_size at byte 24. +constexpr size_t kCdRecordCompressedSizeOffset = 20; +constexpr size_t kCdRecordUncompressedSizeOffset = 24; + +uint32_t ReadU32LE(const std::vector& bytes, size_t offset) { + return static_cast(bytes[offset]) | (static_cast(bytes[offset + 1]) << 8) | + (static_cast(bytes[offset + 2]) << 16) | (static_cast(bytes[offset + 3]) << 24); +} + +} // namespace + +TEST(ExtractZipTest, RejectsCentralDirectorySizeExceedingFileWithoutAllocatingOrWriting) { + // A tiny, otherwise well-formed archive whose EOCD claims a central directory size far larger + // than the whole file — the attacker-controlled `cd_size` field must be validated against the + // real file size before it is ever used to size a std::vector allocation. + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello world")); + auto bytes = builder.Build(); + + PatchU32(bytes, bytes.size() - kEocdRecordSize + kEocdCdSizeOffset, 0x7FFFFFFFu); + auto zip_path = WriteBytesToTempFile(bytes, "fl_zip_extract_huge_cdsize_"); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_FALSE(std::filesystem::exists(dest.path() / "hello.txt")); + // dest.path() itself was pre-created by TempPath::CreateTempDir; what matters is that + // ExtractZip never wrote anything into it before rejecting the malformed cd_size. + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsCentralDirectoryOffsetOutsideFileWithoutAllocatingOrWriting) { + // The EOCD claims a central directory offset beyond the end of the file — combined with a + // plausible cd_size, `cd_offset + cd_size` would run past the archive entirely. + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello world")); + auto bytes = builder.Build(); + + PatchU32(bytes, bytes.size() - kEocdRecordSize + kEocdCdOffsetOffset, 0x10000000u); + auto zip_path = WriteBytesToTempFile(bytes, "fl_zip_extract_bad_cdoffset_"); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_FALSE(std::filesystem::exists(dest.path() / "hello.txt")); + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, NoPartialExtractionWhenOneEntryIsUnsafe) { + // The whole archive is validated before any bytes are written — a single unsafe entry must + // not leave the earlier, otherwise-valid entries behind. + ZipBuilder builder; + builder.AddEntry("good.txt", AsBytes("good")); + builder.AddEntry("../escape.txt", AsBytes("evil")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_FALSE(std::filesystem::exists(dest.path() / "good.txt")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsEntryCompressedSizeLargerThanArchiveWithoutAllocating) { + // A DEFLATE entry whose central-directory compressed_size is rewritten to a huge value. The + // streaming extractor must reject it on the up-front size bound and never attempt an allocation + // or read sized by the attacker-controlled field. + ZipBuilder builder; + builder.AddEntry("big.bin", AsBytes(std::string(256, 'A')), /*compress=*/true); + auto bytes = builder.Build(); + + const uint32_t cd_offset = ReadU32LE(bytes, bytes.size() - kEocdRecordSize + kEocdCdOffsetOffset); + PatchU32(bytes, cd_offset + kCdRecordCompressedSizeOffset, 0x7FFFFFFFu); + + auto zip_path = WriteBytesToTempFile(bytes, "fl_zip_extract_huge_compsize_"); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsStoredEntryWithCompressedUncompressedSizeMismatch) { + // A STORE entry whose central-directory uncompressed_size is rewritten so it no longer equals the + // compressed_size. A STORE entry is copied verbatim, so the two must match — the mismatch is + // rejected during validation, before any extraction begins. + ZipBuilder builder; + builder.AddEntry("a.txt", AsBytes("hello")); // STORE: compressed_size == uncompressed_size == 5 + auto bytes = builder.Build(); + + const uint32_t cd_offset = ReadU32LE(bytes, bytes.size() - kEocdRecordSize + kEocdCdOffsetOffset); + PatchU32(bytes, cd_offset + kCdRecordUncompressedSizeOffset, 4); // now 5 (compressed) != 4 (uncompressed) + + auto zip_path = WriteBytesToTempFile(bytes, "fl_zip_extract_store_mismatch_"); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + } // namespace fl diff --git a/sdk_v2/cpp/test/utils/zip_builder.h b/sdk_v2/cpp/test/utils/zip_builder.h new file mode 100644 index 000000000..9046ddf49 --- /dev/null +++ b/sdk_v2/cpp/test/utils/zip_builder.h @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Shared test helper for hand-assembling minimal ZIP archives (STORE and DEFLATE, with control +// over Unix mode bits) so extraction behavior can be tested without any external zip/tar tool. +#pragma once + +#include "utils/temp_path.h" + +#include + +#include +#include +#include +#include +#include + +namespace fl::test { + +namespace zip_builder_detail { + +inline void AppendU16(std::vector& out, uint16_t value) { + out.push_back(static_cast(value & 0xFF)); + out.push_back(static_cast((value >> 8) & 0xFF)); +} + +inline void AppendU32(std::vector& out, uint32_t value) { + out.push_back(static_cast(value & 0xFF)); + out.push_back(static_cast((value >> 8) & 0xFF)); + out.push_back(static_cast((value >> 16) & 0xFF)); + out.push_back(static_cast((value >> 24) & 0xFF)); +} + +/// Raw-DEFLATEs `data` (no zlib/gzip framing) — matches what real zip archives store. +inline std::vector RawDeflate(const std::vector& data) { + z_stream strm{}; + deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY); + + std::vector out(compressBound(static_cast(data.size())) + 64); + strm.next_in = const_cast(data.data()); + strm.avail_in = static_cast(data.size()); + strm.next_out = out.data(); + strm.avail_out = static_cast(out.size()); + + deflate(&strm, Z_FINISH); + out.resize(out.size() - strm.avail_out); + deflateEnd(&strm); + return out; +} + +} // namespace zip_builder_detail + +class ZipBuilder { + public: + /// @param compress STORE (false) or DEFLATE (true). + /// @param unix_mode Packed into external_attrs high word when host_os == 3 (Unix); 0 to omit. + void AddEntry(const std::string& name, const std::vector& data, bool compress = false, + uint32_t unix_mode = 0) { + Entry entry; + entry.name = name; + entry.crc = crc32(0, data.data(), static_cast(data.size())); + entry.uncompressed_size = static_cast(data.size()); + entry.compression_method = compress ? 8 : 0; + entry.data = compress ? zip_builder_detail::RawDeflate(data) : data; + entry.compressed_size = static_cast(entry.data.size()); + entry.host_os = unix_mode != 0 ? 3 : 0; + entry.external_attrs = unix_mode != 0 ? (unix_mode << 16) : 0; + entries_.push_back(std::move(entry)); + } + + /// Adds an explicit directory entry (name should end with '/'). + void AddDirectory(const std::string& name) { + Entry entry; + entry.name = name; + entry.is_directory = true; + entries_.push_back(std::move(entry)); + } + + std::vector Build() const { + using namespace zip_builder_detail; + std::vector out; + std::vector local_offsets; + + for (const auto& entry : entries_) { + local_offsets.push_back(static_cast(out.size())); + AppendU32(out, 0x04034b50); + AppendU16(out, 20); // version needed + AppendU16(out, 0); // flags + AppendU16(out, entry.compression_method); + AppendU16(out, 0); // mod time + AppendU16(out, 0); // mod date + AppendU32(out, entry.crc); + AppendU32(out, entry.compressed_size); + AppendU32(out, entry.uncompressed_size); + AppendU16(out, static_cast(entry.name.size())); + AppendU16(out, 0); // extra field length + out.insert(out.end(), entry.name.begin(), entry.name.end()); + out.insert(out.end(), entry.data.begin(), entry.data.end()); + } + + const uint32_t cd_offset = static_cast(out.size()); + + for (size_t i = 0; i < entries_.size(); ++i) { + const auto& entry = entries_[i]; + AppendU32(out, 0x02014b50); + out.push_back(0); // version made by (low byte) + out.push_back(entry.host_os); // version made by (high byte = host OS) + AppendU16(out, 20); // version needed + AppendU16(out, 0); // flags + AppendU16(out, entry.compression_method); + AppendU16(out, 0); // mod time + AppendU16(out, 0); // mod date + AppendU32(out, entry.crc); + AppendU32(out, entry.compressed_size); + AppendU32(out, entry.uncompressed_size); + AppendU16(out, static_cast(entry.name.size())); + AppendU16(out, 0); // extra field length + AppendU16(out, 0); // comment length + AppendU16(out, 0); // disk number start + AppendU16(out, 0); // internal attrs + AppendU32(out, entry.external_attrs); + AppendU32(out, local_offsets[i]); + out.insert(out.end(), entry.name.begin(), entry.name.end()); + } + + const uint32_t cd_size = static_cast(out.size()) - cd_offset; + + AppendU32(out, 0x06054b50); + AppendU16(out, 0); // disk number + AppendU16(out, 0); // cd start disk + AppendU16(out, static_cast(entries_.size())); + AppendU16(out, static_cast(entries_.size())); + AppendU32(out, cd_size); + AppendU32(out, cd_offset); + AppendU16(out, 0); // comment length + + return out; + } + + std::filesystem::path WriteToTempFile() const { + auto path = MakeUniqueTempPath("fl_zip_builder_"); + auto bytes = Build(); + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + return path; + } + + void WriteToFile(const std::filesystem::path& path) const { + auto bytes = Build(); + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + } + + private: + struct Entry { + std::string name; + std::vector data; + uint32_t crc = 0; + uint32_t compressed_size = 0; + uint32_t uncompressed_size = 0; + uint16_t compression_method = 0; + uint32_t external_attrs = 0; + uint8_t host_os = 0; + bool is_directory = false; + }; + + std::vector entries_; +}; + +} // namespace fl::test diff --git a/sdk_v2/cpp/vcpkg.json b/sdk_v2/cpp/vcpkg.json index 8a3d1037e..1dfee9a61 100644 --- a/sdk_v2/cpp/vcpkg.json +++ b/sdk_v2/cpp/vcpkg.json @@ -5,6 +5,10 @@ "builtin-baseline": "256acc64012b23a13041d8705805e1f23b43a024", "dependencies": [ "azure-storage-blobs-cpp", + { + "name": "libarchive", + "default-features": false + }, { "name": "azure-core-cpp", "default-features": false, @@ -18,6 +22,7 @@ "platform": "!windows | uwp" }, "ms-gsl", + "zlib", "nlohmann-json", "spdlog" ], From 5182b0a951a9fd5052478048f8bd07c3da1a692e Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Tue, 4 Aug 2026 23:14:07 +0000 Subject: [PATCH 02/11] Update --- .../src/ep_detection/cuda_ep_bootstrapper.cc | 41 +----- sdk_v2/cpp/src/ep_detection/ep_utils.cc | 58 ++++++++ sdk_v2/cpp/src/ep_detection/ep_utils.h | 38 ++++++ .../ep_detection/webgpu_ep_bootstrapper.cc | 7 +- sdk_v2/cpp/test/CMakeLists.txt | 1 + sdk_v2/cpp/test/internal_api/ep_utils_test.cc | 129 ++++++++++++++++++ .../webgpu_ep_bootstrapper_test.cc | 58 +++++++- 7 files changed, 288 insertions(+), 44 deletions(-) create mode 100644 sdk_v2/cpp/test/internal_api/ep_utils_test.cc diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 679683731..c56fae2d3 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "ep_detection/cuda_ep_bootstrapper.h" +#include "ep_detection/ep_utils.h" #include "ep_detection/nvml_gpu_detector.h" #include "logger.h" #include "utils.h" @@ -12,10 +13,7 @@ #include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#elif defined(__linux__) && !defined(__ANDROID__) +#if defined(__linux__) && !defined(__ANDROID__) #include #endif @@ -97,39 +95,6 @@ std::optional BuildCudaManifest() { #endif } -#ifdef _WIN32 -bool IsCoreRuntimeLibrary(const std::filesystem::path& filename) { - return _wcsicmp(filename.c_str(), L"onnxruntime.dll") == 0 || - _wcsicmp(filename.c_str(), L"onnxruntime-genai.dll") == 0; -} - -bool LoadBundleDependencies(const std::filesystem::path& bin_dir, - const fl::EpBundleManifest& manifest, - fl::ILogger& logger) { - for (const auto& artifact : manifest.artifacts) { - for (const auto& file : artifact.extracted_files) { - const auto path = bin_dir / file.relative_path; - if (_wcsicmp(path.extension().c_str(), L".dll") != 0 || - _wcsicmp(path.filename().c_str(), - std::filesystem::path(manifest.provider_relative_path).filename().c_str()) == 0 || - IsCoreRuntimeLibrary(path.filename())) { - continue; - } - - if (!LoadLibraryExW(path.c_str(), nullptr, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)) { - logger.Log(fl::LogLevel::Warning, - fmt::format("CUDA EP: failed to load dependency '{}' ({})", - path.string(), GetLastError())); - return false; - } - } - } - - return true; -} -#endif - #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) bool LoadGenAiCudaLibrary(const std::filesystem::path& path, void*& handle, fl::ILogger& logger) { if (handle) { @@ -247,7 +212,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - if (!LoadBundleDependencies(txn->bin_dir(), *manifest, logger)) { + if (!LoadEpBundleDependencies(txn->bin_dir(), *manifest, "CUDA EP", logger)) { return false; } #elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc index 5cd025b1b..fa3bd490e 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -17,6 +17,19 @@ namespace fl { +namespace { + +// The provider library itself and the core ORT runtime are handled separately from bundle +// dependencies: the provider is loaded by `RegisterExecutionProviderLibrary`, and the core runtime is +// already loaded by the host process, so neither should be preloaded again here. +bool IsCoreRuntimeLibrary(const std::filesystem::path& filename) { + const auto name = filename.string(); + return CompareCaseInsensitive(name, "onnxruntime.dll") == 0 || + CompareCaseInsensitive(name, "onnxruntime-genai.dll") == 0; +} + +} // namespace + bool VerifyEpArchive( const std::filesystem::path& archive_path, std::string_view expected_hash, @@ -90,4 +103,49 @@ void PrependDirToProcessPath([[maybe_unused]] const std::filesystem::path& dir) #endif } +std::vector SelectEpBundleDependenciesToPreload( + const std::filesystem::path& bin_dir, + const EpBundleManifest& manifest) { + std::vector dependencies; + const auto provider_filename = std::filesystem::path(manifest.provider_relative_path).filename().string(); + + for (const auto& artifact : manifest.artifacts) { + for (const auto& file : artifact.extracted_files) { + const auto path = std::filesystem::absolute(bin_dir / file.relative_path); + + if (CompareCaseInsensitive(path.extension().string(), ".dll") != 0 || + CompareCaseInsensitive(path.filename().string(), provider_filename) == 0 || + IsCoreRuntimeLibrary(path.filename())) { + continue; + } + + dependencies.push_back(path); + } + } + + return dependencies; +} + +bool LoadEpBundleDependencies( + [[maybe_unused]] const std::filesystem::path& bin_dir, + [[maybe_unused]] const EpBundleManifest& manifest, + [[maybe_unused]] std::string_view ep_name, + [[maybe_unused]] ILogger& logger) { +#ifdef _WIN32 + for (const auto& path : SelectEpBundleDependenciesToPreload(bin_dir, manifest)) { + if (!LoadLibraryExW(path.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: failed to load dependency '{}' ({})", + ep_name, path.string(), GetLastError())); + return false; + } + } +#endif + // Preloading is a Windows-specific concern (LoadLibraryExW search-path flags); other platforms rely + // on RPATH/PATH-style resolution and don't need this step. + + return true; +} + } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h index ba4690e65..8fcb27ed8 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -2,10 +2,13 @@ // Licensed under the MIT License. #pragma once +#include "ep_detection/ep_bundle_manifest.h" + #include #include #include #include +#include namespace fl { @@ -47,4 +50,39 @@ bool VerifyEpBinaries( /// @param dir Directory to prepend to `PATH`. void PrependDirToProcessPath(const std::filesystem::path& dir); +/// Select the manifest-declared DLLs under @p bin_dir that should be preloaded before the EP provider +/// library is registered, excluding the provider library itself and the core ORT runtime libraries +/// (`onnxruntime.dll`, `onnxruntime-genai.dll`), matched case-insensitively. +/// +/// This selection logic is platform-independent (pure path/string manipulation) so it can be unit +/// tested on any platform, even though the actual preloading only happens on Windows. +/// +/// @param bin_dir Directory containing the extracted bundle files. +/// @param manifest Bundle manifest describing the extracted artifacts. +/// @return Absolute paths of the DLLs that should be preloaded, in manifest order. +std::vector SelectEpBundleDependenciesToPreload( + const std::filesystem::path& bin_dir, + const EpBundleManifest& manifest); + +/// Preload the non-provider, non-core-runtime DLLs declared by @p manifest from @p bin_dir. +/// +/// EP provider libraries (CUDA, WebGPU) can implicitly or delay-load sibling dependency DLLs, and +/// `RegisterExecutionProviderLibrary` loads the provider DLL eagerly. Preloading those dependencies by +/// absolute path with `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32` ensures they +/// resolve correctly regardless of the process `PATH`, before the provider DLL is registered. +/// +/// This is a no-op that returns true on non-Windows platforms. +/// +/// @param bin_dir Directory containing the extracted bundle files. +/// @param manifest Bundle manifest describing the extracted artifacts. +/// @param ep_name EP name used in warning log messages (e.g. "CUDA EP"). +/// @param logger Logger for diagnostic output. +/// @return true if every selected dependency loaded successfully (or none needed loading); false +/// otherwise. +bool LoadEpBundleDependencies( + const std::filesystem::path& bin_dir, + const EpBundleManifest& manifest, + std::string_view ep_name, + ILogger& logger); + } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index 9c5294915..de7229cb4 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -182,10 +182,15 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - // The provider delay-loads sibling DirectX compiler binaries after registration. + // The provider delay-loads sibling DirectX compiler binaries after registration; keep PATH + // primed as a fallback in addition to the explicit preload below. PrependDirToProcessPath(txn->bin_dir()); #endif + if (!LoadEpBundleDependencies(txn->bin_dir(), *manifest, "WebGPU EP", logger)) { + return false; + } + if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "WebGPU EP: ORT registration failed"); return false; diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index 8443a1d4c..a33e6d592 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(foundry_local_tests internal_api/embeddings/fp16_test.cc internal_api/ep_bundle_installer_test.cc internal_api/ep_detector_test.cc + internal_api/ep_utils_test.cc internal_api/exception_test.cc internal_api/execution_provider_test.cc internal_api/file_lock_test.cc diff --git a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc new file mode 100644 index 000000000..03467525d --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/ep_utils.h" + +#include "logger.h" + +#include + +#include +#include + +namespace fl { + +namespace { + +class NullLogger : public ILogger { + public: + void Log(LogLevel /*level*/, std::string_view /*message*/) override {} +}; + +// A representative manifest mixing the provider DLL, non-DLL manifest entries, core ORT runtime DLLs +// (in mixed case, to exercise case-insensitive exclusion), and DLL dependencies that should be +// preloaded, spread across more than one artifact. +EpBundleManifest MakeCudaLikeManifest() { + EpBundleManifest manifest; + manifest.bundle_id = "test-cuda-ep"; + manifest.provider_relative_path = "onnxruntime_providers_cuda.dll"; + manifest.artifacts = { + EpBundleArtifact{ + .id = "cuda-ep", + .url = "https://example.test/cuda-ep.zip", + .is_archive = true, + .archive_sha256 = "archive-hash", + .extracted_files = + { + {.relative_path = "onnxruntime_providers_cuda.dll", .sha256 = "a"}, + {.relative_path = "cudart64_12.dll", .sha256 = "b"}, + {.relative_path = "ONNXRUNTIME.DLL", .sha256 = "c"}, + {.relative_path = "onnxruntime-genai.dll", .sha256 = "d"}, + {.relative_path = "version.json", .sha256 = "e"}, + }, + .archive_max_bytes = 0, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }, + EpBundleArtifact{ + .id = "cuda-toolkit", + .url = "https://example.test/cuda-toolkit.zip", + .is_archive = true, + .archive_sha256 = "archive-hash", + .extracted_files = + { + {.relative_path = "cublas64_12.DLL", .sha256 = "f"}, + }, + .archive_max_bytes = 0, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }, + }; + return manifest; +} + +} // namespace + +TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadExcludesProviderAndCoreRuntimeCaseInsensitively) { + const std::filesystem::path bin_dir = std::filesystem::path("opt") / "foundry" / "cuda-ep" / "bin"; + const auto manifest = MakeCudaLikeManifest(); + + const auto dependencies = SelectEpBundleDependenciesToPreload(bin_dir, manifest); + + // Only the two non-provider, non-core-runtime DLLs should remain, in manifest order, with + // version.json (not a DLL) and the provider/core runtime DLLs (matched case-insensitively) excluded. + ASSERT_EQ(dependencies.size(), 2u); + EXPECT_EQ(dependencies[0], std::filesystem::absolute(bin_dir / "cudart64_12.dll")); + EXPECT_EQ(dependencies[1], std::filesystem::absolute(bin_dir / "cublas64_12.DLL")); + EXPECT_TRUE(dependencies[0].is_absolute()); + EXPECT_TRUE(dependencies[1].is_absolute()); +} + +TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadReturnsEmptyWhenOnlyProviderAndCoreRuntimePresent) { + EpBundleManifest manifest; + manifest.bundle_id = "test-webgpu-ep"; + manifest.provider_relative_path = "onnxruntime_providers_webgpu.dll"; + manifest.artifacts = { + EpBundleArtifact{ + .id = "webgpu-ep", + .url = "https://example.test/webgpu-ep.zip", + .is_archive = true, + .archive_sha256 = "archive-hash", + .extracted_files = + { + {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = "a"}, + {.relative_path = "onnxruntime.dll", .sha256 = "b"}, + {.relative_path = "version.json", .sha256 = "c"}, + }, + .archive_max_bytes = 0, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }, + }; + + EXPECT_TRUE(SelectEpBundleDependenciesToPreload("bin", manifest).empty()); +} + +TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadHandlesManifestWithNoArtifacts) { + EpBundleManifest manifest; + manifest.bundle_id = "empty"; + manifest.provider_relative_path = "provider.dll"; + + EXPECT_TRUE(SelectEpBundleDependenciesToPreload("bin", manifest).empty()); +} + +TEST(EpUtilsTest, LoadEpBundleDependenciesIsNoOpTrueOnNonWindows) { +#ifndef _WIN32 + NullLogger logger; + const auto manifest = MakeCudaLikeManifest(); + + // LoadEpBundleDependencies is documented as a no-op that always returns true on non-Windows + // platforms; neither the directory nor the dependency files need to exist here. + EXPECT_TRUE(LoadEpBundleDependencies("nonexistent/bin/dir", manifest, "Test EP", logger)); +#else + GTEST_SKIP() << "Preloading behavior is exercised on Windows only."; +#endif +} + +} // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc index 798b2c5e3..7048a3f0e 100644 --- a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -20,6 +22,32 @@ namespace fl { namespace { constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; +constexpr const char* kScopedEnvironmentVariableTestEnv = "FOUNDRY_LOCAL_SCOPED_ENVIRONMENT_VARIABLE_TEST"; + +std::optional GetEnvValue(const char* name) { +#ifdef _WIN32 + char* value = nullptr; + size_t length = 0; + const auto error = _dupenv_s(&value, &length, name); + const std::unique_ptr buffer(value, &std::free); + if (error != 0) { + throw std::system_error(error, std::generic_category(), "_dupenv_s failed"); + } + + if (buffer == nullptr) { + return std::nullopt; + } + + return std::string(buffer.get()); +#else + const auto* value = std::getenv(name); + if (value == nullptr) { + return std::nullopt; + } + + return std::string(value); +#endif +} void SetEnvValue(const char* name, const std::optional& value) { #ifdef _WIN32 @@ -36,11 +64,8 @@ void SetEnvValue(const char* name, const std::optional& value) { class ScopedEnvironmentVariable { public: ScopedEnvironmentVariable(const char* name, std::string value) - : name_(name) { - if (const auto* previous = std::getenv(name); previous != nullptr) { - previous_ = previous; - } - + : name_(name), + previous_(GetEnvValue(name)) { SetEnvValue(name_, value); } @@ -67,6 +92,29 @@ TEST(WebGpuEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { #endif } +TEST(WebGpuEpBootstrapperTest, ScopedEnvironmentVariableRestoresExistingValue) { + ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); + + { + ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); + EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "during"); + } + + EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "before"); +} + +TEST(WebGpuEpBootstrapperTest, ScopedEnvironmentVariableRestoresUnsetValue) { + ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); + SetEnvValue(kScopedEnvironmentVariableTestEnv, std::nullopt); + + { + ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); + EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "during"); + } + + EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), std::nullopt); +} + TEST(WebGpuEpBootstrapperTest, OverrideRegistersUsingExistingProviderConvention) { auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); const auto provider_path = root.path() / "custom_webgpu_provider"; From dd0720000a985f225d183d27f5d2e71d20f8ccf5 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Wed, 5 Aug 2026 18:43:22 +0000 Subject: [PATCH 03/11] Simplify and correct bootstrappable eps --- sdk_v2/cpp/CMakeLists.txt | 7 +- .../src/ep_detection/cuda_ep_bootstrapper.cc | 134 ++----- .../src/ep_detection/cuda_ep_bootstrapper.h | 6 +- .../cpp/src/ep_detection/cuda_ep_manifest.cc | 168 +++++++++ .../cpp/src/ep_detection/cuda_ep_manifest.h | 21 ++ .../src/ep_detection/ep_bundle_installer.cc | 304 +++++++++++----- .../cpp/src/ep_detection/ep_bundle_manifest.h | 1 + sdk_v2/cpp/src/ep_detection/ep_utils.cc | 109 ++---- sdk_v2/cpp/src/ep_detection/ep_utils.h | 67 +--- .../cpp/src/ep_detection/nvml_gpu_detector.cc | 39 ++- .../ep_detection/webgpu_ep_bootstrapper.cc | 65 ++-- .../src/ep_detection/webgpu_ep_bootstrapper.h | 6 +- sdk_v2/cpp/src/http/http_download.cc | 68 ++-- sdk_v2/cpp/src/http/http_download.h | 12 +- sdk_v2/cpp/src/manager.cc | 165 ++++----- sdk_v2/cpp/src/manager.h | 20 +- sdk_v2/cpp/src/util/zip_extract.cc | 100 ++++-- sdk_v2/cpp/test/CMakeLists.txt | 1 + .../internal_api/cuda_ep_bootstrapper_test.cc | 187 ++++++++++ .../internal_api/ep_bundle_installer_test.cc | 331 ++++++++++++++++-- sdk_v2/cpp/test/internal_api/ep_utils_test.cc | 17 +- .../test/internal_api/http_download_test.cc | 32 +- .../webgpu_ep_bootstrapper_test.cc | 104 ++---- .../cpp/test/internal_api/zip_extract_test.cc | 145 +++++--- .../test/utils/scoped_environment_variable.h | 72 ++++ sdk_v2/cpp/test/utils/zip_builder.h | 2 - 26 files changed, 1475 insertions(+), 708 deletions(-) create mode 100644 sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc create mode 100644 sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.h create mode 100644 sdk_v2/cpp/test/utils/scoped_environment_variable.h diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 22954f2a7..d05efcfe4 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -65,11 +65,7 @@ find_package(nlohmann_json CONFIG REQUIRED) find_package(azure-storage-blobs-cpp CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) find_package(Microsoft.GSL CONFIG REQUIRED) -# Used by util/zip_extract.cc for in-process, bounded EP archive extraction (no shelling out -# to tar/unzip). Already a transitive dependency of azure-core-cpp; declared directly since we -# now link against it explicitly. find_package(LibArchive REQUIRED) -find_package(ZLIB REQUIRED) if(FOUNDRY_LOCAL_BUILD_SERVICE) find_package(oatpp CONFIG REQUIRED) @@ -77,6 +73,7 @@ endif() if(FOUNDRY_LOCAL_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) + find_package(ZLIB REQUIRED) enable_testing() endif() @@ -166,6 +163,7 @@ set(FOUNDRY_LOCAL_SOURCES src/download/inference_model_writer.cc src/download/model_registry_client.cc src/ep_detection/cuda_ep_bootstrapper.cc + src/ep_detection/cuda_ep_manifest.cc src/ep_detection/ep_bundle_installer.cc src/ep_detection/ep_detector.cc src/ep_detection/ep_utils.cc @@ -247,7 +245,6 @@ function(foundry_local_configure_target TARGET LINK_SCOPE) Azure::azure-storage-blobs spdlog::spdlog LibArchive::LibArchive - ZLIB::ZLIB ${CMAKE_DL_LIBS} ) diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index c56fae2d3..f92841101 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "ep_detection/cuda_ep_bootstrapper.h" +#include "ep_detection/cuda_ep_manifest.h" #include "ep_detection/ep_utils.h" #include "ep_detection/nvml_gpu_detector.h" #include "logger.h" @@ -10,7 +11,6 @@ #include #include -#include #include #if defined(__linux__) && !defined(__ANDROID__) @@ -27,71 +27,17 @@ constexpr const char* kCudaProviderOverrideEnv = "FOUNDRY_LOCAL_CUDA_EP_LIBRARY" constexpr const char* kGenAiCudaLibrary = "libonnxruntime-genai-cuda.so"; #endif +fl::CudaEpPlatform HostCudaEpPlatform() { #if defined(_WIN32) && defined(_M_ARM64) -constexpr const char* kCudaBundleId = "cuda-ep-win-arm64-unconfigured"; + return fl::CudaEpPlatform::WindowsArm64; #elif defined(_WIN32) && defined(_M_X64) -constexpr const char* kCudaBundleId = "cuda-ep-win-x64-unconfigured"; + return fl::CudaEpPlatform::WindowsX64; #elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) -constexpr const char* kCudaBundleId = "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.1-20260804-074520"; -constexpr const char* kCudaDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/cuda-ep-linux-x64-20260804-074520.zip"; -constexpr const char* kCudaArchiveSha256 = - "97FF54C93A8E4D6622905AD19BCC9D6B5AA03E54B6B38682FC51117086DCF1F6"; -constexpr uint64_t kCudaArchiveMaxBytes = 512ULL * 1024 * 1024; -#endif - -#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) -fl::EpBundleArtifact DisabledArchiveArtifact(std::string id) { - return fl::EpBundleArtifact{.id = std::move(id), - .url = "", - .is_archive = true, - .archive_sha256 = "", - .extracted_files = {}, - .archive_max_bytes = 0, - .raw_relative_path = "", - .raw_sha256 = "", - .raw_max_bytes = 0}; -} -#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) -fl::EpBundleArtifact LinuxCudaArchiveArtifact() { - return fl::EpBundleArtifact{ - .id = "cuda-ep", - .url = kCudaDownloadUrl, - .is_archive = true, - .archive_sha256 = kCudaArchiveSha256, - .extracted_files = - { - {.relative_path = "libonnxruntime-genai-cuda.so", - .sha256 = "86AED826BC9221ABA24A1B9C856A403FD8AEC082B06E6924F616E08A49C6C2F0"}, - {.relative_path = "libonnxruntime_providers_cuda.so", - .sha256 = "9418788F29E45F70904DBA8FA21BE7317C92A45D505B1E50322F3B71A94E52F7"}, - {.relative_path = "version.json", - .sha256 = "65133BC2003C363B4D2C6CB85BC913AFD5291B1D6E2869C3656D940DBF72A505"}, - }, - .archive_max_bytes = kCudaArchiveMaxBytes, - .raw_relative_path = "", - .raw_sha256 = "", - .raw_max_bytes = 0, - }; -} -#endif - -std::optional BuildCudaManifest() { -#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) - fl::EpBundleManifest manifest; - manifest.bundle_id = kCudaBundleId; - manifest.provider_relative_path = "onnxruntime_providers_cuda.dll"; - manifest.artifacts = {DisabledArchiveArtifact("cuda-toolkit"), DisabledArchiveArtifact("cudnn"), - DisabledArchiveArtifact("cuda-ep")}; - return manifest; -#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) - fl::EpBundleManifest manifest; - manifest.bundle_id = kCudaBundleId; - manifest.provider_relative_path = "libonnxruntime_providers_cuda.so"; - manifest.artifacts = {LinuxCudaArchiveArtifact()}; - return manifest; + return fl::CudaEpPlatform::LinuxX64; +#elif defined(__linux__) && defined(__aarch64__) && !defined(__ANDROID__) + return fl::CudaEpPlatform::LinuxArm64; #else - return std::nullopt; + return fl::CudaEpPlatform::Unsupported; #endif } @@ -119,8 +65,7 @@ bool LoadGenAiCudaLibrary(const std::filesystem::path& path, void*& handle, fl:: namespace fl { CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) - : register_ep_(std::move(register_ep)), - installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP") {} + : register_ep_(std::move(register_ep)), installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP") {} CudaEpBootstrapper::~CudaEpBootstrapper() { #if defined(__linux__) && !defined(__ANDROID__) @@ -130,17 +75,11 @@ CudaEpBootstrapper::~CudaEpBootstrapper() { #endif } -const std::string& CudaEpBootstrapper::Name() const { - return name_; -} +const std::string& CudaEpBootstrapper::Name() const { return name_; } -bool CudaEpBootstrapper::IsRegistered() const { - return registered_; -} +bool CudaEpBootstrapper::IsRegistered() const { return registered_; } -bool CudaEpBootstrapper::DownloadAndRegister(bool force, - const ProgressCallback& progress_cb, - ILogger& logger) { +bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) { if (registered_ && !force) { if (progress_cb) { progress_cb(name_, 100.0f); @@ -161,16 +100,19 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, std::filesystem::path provider_path = std::filesystem::absolute(*override_path); if (!std::filesystem::exists(provider_path)) { - logger.Log(LogLevel::Warning, - fmt::format("CUDA EP: {} set but file does not exist ({})", - kCudaProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Warning, fmt::format("CUDA EP: {} set but file does not exist ({})", + kCudaProviderOverrideEnv, provider_path.string())); return false; } - if (progress_cb) { - progress_cb(name_, 90.0f); + if (progress_cb && !progress_cb(name_, 90.0f)) { + return false; } +#ifdef _WIN32 + PrependDirToProcessPath(provider_path.parent_path()); +#endif + #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_handle_, logger)) { return false; @@ -178,9 +120,8 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, #endif if (!register_ep_(kRegistrationName, provider_path)) { - logger.Log(LogLevel::Warning, - fmt::format("CUDA EP: ORT registration failed for override {}={}", - kCudaProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Warning, fmt::format("CUDA EP: ORT registration failed for override {}={}", + kCudaProviderOverrideEnv, provider_path.string())); return false; } @@ -190,29 +131,31 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, progress_cb(name_, 100.0f); } - logger.Log(LogLevel::Information, - fmt::format("CUDA EP: ready (override_env={} install_path={})", - kCudaProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Information, fmt::format("CUDA EP: ready (override_env={} install_path={})", + kCudaProviderOverrideEnv, provider_path.string())); return true; } - auto manifest = BuildCudaManifest(); + auto manifest = BuildCudaEpManifest(HostCudaEpPlatform()); if (!manifest.has_value()) { logger.Log(LogLevel::Warning, "CUDA EP: no bundle available for this platform"); return false; } - // CUDA force requests another registration attempt, but retains the existing package reuse behavior. - auto txn = installer_.EnsureInstalled(*manifest, progress_cb, logger, - EpBundleInstallPolicy::ReuseVerified); + const auto install_policy = force ? EpBundleInstallPolicy::ForceDownload : EpBundleInstallPolicy::ReuseVerified; + auto txn = installer_.EnsureInstalled(*manifest, progress_cb, logger, install_policy); if (!txn) { return false; } - auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + if (!txn->CommitActive(logger)) { + logger.Log(LogLevel::Warning, "CUDA EP: failed to publish active bundle marker"); + return false; + } + const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - if (!LoadEpBundleDependencies(txn->bin_dir(), *manifest, "CUDA EP", logger)) { + if (!dependency_owner_.Load(txn->bin_dir(), *manifest, "CUDA EP", logger)) { return false; } #elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) @@ -228,16 +171,11 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, registered_ = true; - if (!txn->CommitActive(logger)) { - logger.Log(LogLevel::Warning, "CUDA EP: failed to publish active bundle marker"); - } - if (progress_cb) { progress_cb(name_, 100.0f); } - logger.Log(LogLevel::Information, - fmt::format("CUDA EP: ready (install_path={})", txn->bin_dir().string())); + logger.Log(LogLevel::Information, fmt::format("CUDA EP: ready (install_path={})", txn->bin_dir().string())); return true; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("CUDA EP: error: {}", e.what())); @@ -245,9 +183,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, } } -bool CudaEpBootstrapper::HasNvidiaGpu() { - return NvmlGpuDetector::HasNvidiaGpu(); -} +bool CudaEpBootstrapper::HasNvidiaGpu() { return NvmlGpuDetector::HasNvidiaGpu(); } bool CudaEpBootstrapper::IsSupportedPlatform() { #if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index d533d314b..cb0ca2c15 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -5,6 +5,7 @@ #include "ep_detection/ep_bootstrapper.h" #include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" +#include "ep_detection/ep_utils.h" #include @@ -28,9 +29,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { const std::string& Name() const override; bool IsRegistered() const override; - bool DownloadAndRegister(bool force, - const ProgressCallback& progress_cb, - ILogger& logger) override; + bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) override; /// Check for an NVIDIA GPU with compute capability >= 5.0 using NVML. static bool HasNvidiaGpu(); @@ -44,6 +43,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { int attempts_ = 0; EpRegistrationCallback register_ep_; EpBundleInstaller installer_; + EpBundleDependencyOwner dependency_owner_; #if defined(__linux__) && !defined(__ANDROID__) void* genai_cuda_handle_ = nullptr; #endif diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc new file mode 100644 index 000000000..aca40a31c --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/cuda_ep_manifest.h" + +#include +#include +#include +#include + +namespace fl { + +namespace { + +constexpr const char* kCdnBase = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/"; +constexpr uint64_t kMiB = 1024ULL * 1024; + +EpBundleArtifact Archive(std::string id, std::string filename, std::string sha256, uint64_t max_bytes, + std::vector files) { + return EpBundleArtifact{ + .id = std::move(id), + .url = std::string(kCdnBase) + filename, + .is_archive = true, + .archive_sha256 = std::move(sha256), + .extracted_files = std::move(files), + .ignored_archive_paths = {"version.json"}, + .archive_max_bytes = max_bytes, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }; +} + +EpBundleManifest WindowsX64Manifest() { + return EpBundleManifest{ + .bundle_id = "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260805-050438", + .artifacts = + { + Archive( + "cuda-toolkit", "cuda-bins-win-x64-20260805-050438.zip", + "b47716cbd9a1c92722a6bc914ca57b0e8efea15f7b1a46eecfb2637cadc1bee5", 640 * kMiB, + { + {.relative_path = "cublas64_12.dll", + .sha256 = "9513540e4ec4c51ee9e7304138c2cc255c29a8c181f9e80c38efa25738becd99"}, + {.relative_path = "cublasLt64_12.dll", + .sha256 = "b199d1ff892a81b7fd3d57ba1781549609b41500b36008fef326038393ad46c7"}, + {.relative_path = "cudart64_12.dll", + .sha256 = "c2c9a9c22a9bcba90e261825968836787b331038047a26770cffb7a583c28344"}, + }), + Archive( + "cudnn", "cudnn-bins-win-x64-20260805-050438.zip", + "1b065e115c2ac35040053ebe594a8c089906f8cbe5b8d8ed832ba5eb27cdeb5e", 704 * kMiB, + { + {.relative_path = "cudnn64_9.dll", + .sha256 = "0d1d71325eb5e91570ab8ba8e399e07bf717ffd76511b2407229a8f45e0b1305"}, + {.relative_path = "cudnn_adv64_9.dll", + .sha256 = "6d66bce22502c2582a9c0e5398ee8cc38addce2c837eb6db8786abc650e48dd8"}, + {.relative_path = "cudnn_engines_precompiled64_9.dll", + .sha256 = "b410c3b42921afc6e668ff994fce1bf12c5a8a9b1a9445ebee61958bf49b1e0a"}, + {.relative_path = "cudnn_engines_runtime_compiled64_9.dll", + .sha256 = "8e62214495c96b93c6333c084fec49b43f272b7e1977a12fe62275e9070647eb"}, + {.relative_path = "cudnn_graph64_9.dll", + .sha256 = "82f710b01d15d20c311009721c771b76360a4954ebf7b5f4a407b0f96587f568"}, + {.relative_path = "cudnn_heuristic64_9.dll", + .sha256 = "50719eefb6692074096bf83c87e9cd186f7ce5b953201da33669c1277a61949b"}, + {.relative_path = "cudnn_ops64_9.dll", + .sha256 = "49487537744256a3d4365c4792b03bf31130ad1faea0a13eafa219620941d837"}, + }), + Archive( + "cuda-ep", "cuda-ep-bins-win-x64-20260805-050438.zip", + "65044a715a2d4b74e77f019988f77c936f5b62973c27cf2d59704cf39057e567", 256 * kMiB, + { + {.relative_path = "onnxruntime-genai-cuda.dll", + .sha256 = "612ad6cf3d099431af886537080223a62522e58caba0c7d278b9b4b1eb03c4ce"}, + {.relative_path = "onnxruntime_providers_cuda.dll", + .sha256 = "971c1002ce7c16338273f693316bec4862ac74c7efa2ffbd644630cfe10d6e37"}, + }), + }, + .provider_relative_path = "onnxruntime_providers_cuda.dll", + }; +} + +EpBundleManifest WindowsArm64Manifest() { + return EpBundleManifest{ + .bundle_id = "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260805-050639", + .artifacts = + { + Archive( + "cuda-toolkit", "cuda-bins-win-arm64-20260805-050639.zip", + "b4e0ce6beea87843d02c7d41e04fee3d1a9fb22e0f4fb5e587914cf4f4b94113", 192 * kMiB, + { + {.relative_path = "cublas64_13.dll", + .sha256 = "80b322ce3fe77d1c6c0348e30a31c5f2682da4197680177a179af69275b57997"}, + {.relative_path = "cublasLt64_13.dll", + .sha256 = "d13048a5f17deeb1a051189c0d5ac898cdf398c6dfca62d100c6eb39329a1d80"}, + {.relative_path = "cudart64_13.dll", + .sha256 = "32504bd5f424a4e73d3bb5ecc69f018538ae371efa0210bd33e88c7c78b9dca7"}, + }), + Archive( + "cudnn", "cudnn-bins-win-arm64-20260805-050639.zip", + "24347fc6b596ae28c32659c82da688bc386da36228e65329df031c028d8527ad", 192 * kMiB, + { + {.relative_path = "cudnn64_9.dll", + .sha256 = "247cecbb33132c829c6ed328b7dd34d077a27d0f0fb0ee0b56469ec6bdfd1c17"}, + {.relative_path = "cudnn_adv64_9.dll", + .sha256 = "b624590960a3ce3ac7c3a5fc683912dbd9ba9de20fa1af52db4485c435c78375"}, + {.relative_path = "cudnn_engines_precompiled64_9.dll", + .sha256 = "c3be7f8a9091865b7fc94ddd69e62024338d42a188633729e4520244b072da2d"}, + {.relative_path = "cudnn_engines_runtime_compiled64_9.dll", + .sha256 = "bd558d60e1dbeeeee8f59dfec8bd5ce992876f923e2f19f7ef03a4a7e110a89a"}, + {.relative_path = "cudnn_graph64_9.dll", + .sha256 = "8f568df300b0733abe2cb35ea6bfcc40d2330db005ab9ebba96d008f6bc0b568"}, + {.relative_path = "cudnn_heuristic64_9.dll", + .sha256 = "59d5aad876ab55d30194f36d3f2c5ff90eeaeed502b256c83ed3d6082030f58d"}, + {.relative_path = "cudnn_ops64_9.dll", + .sha256 = "c9e0ec0e0a4e659393e15897ed1f6e5bac677e0c0fe7e12290f0386f19477b6b"}, + }), + Archive( + "cuda-ep", "cuda-ep-bins-win-arm64-20260805-050639.zip", + "8152d03a0fbef39bd11f5b07dbc6776abd125dbee1dc1d2877a04dc62bbde641", 96 * kMiB, + { + {.relative_path = "onnxruntime-genai-cuda.dll", + .sha256 = "5284fdec9d4e9e25d6b4cf129205f0c88d3c2f5e678907b2bc1581b575266016"}, + {.relative_path = "onnxruntime_providers_cuda.dll", + .sha256 = "b60cd5a26bc180229c9da0dc635d6b3404c306246708291dfae7c9f72ad5e862"}, + }), + }, + .provider_relative_path = "onnxruntime_providers_cuda.dll", + }; +} + +EpBundleManifest LinuxX64Manifest() { + return EpBundleManifest{ + .bundle_id = "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260805-050706", + .artifacts = + { + Archive( + "cuda-ep", "cuda-ep-linux-x64-20260805-050706.zip", + "2bc3e5949b75d7521d903c958716c06602ddaa5c2a1f98bd12811294db738c37", 448 * kMiB, + { + {.relative_path = "libonnxruntime-genai-cuda.so", + .sha256 = "8b26db7a085de61653ebaaa8fc221b720879fe74583eb01204f11bf22638c345"}, + {.relative_path = "libonnxruntime_providers_cuda.so", + .sha256 = "da94d951b89dc84c44b10f7faf52b17e675b3f1a13d8f32808264d425d0464bd"}, + }), + }, + .provider_relative_path = "libonnxruntime_providers_cuda.so", + }; +} + +} // namespace + +std::optional BuildCudaEpManifest(CudaEpPlatform platform) { + switch (platform) { + case CudaEpPlatform::WindowsX64: + return WindowsX64Manifest(); + case CudaEpPlatform::WindowsArm64: + return WindowsArm64Manifest(); + case CudaEpPlatform::LinuxX64: + return LinuxX64Manifest(); + case CudaEpPlatform::LinuxArm64: + case CudaEpPlatform::Unsupported: + return std::nullopt; + } + + return std::nullopt; +} + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.h new file mode 100644 index 000000000..b1092f1ba --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include "ep_detection/ep_bundle_manifest.h" + +#include + +namespace fl { + +enum class CudaEpPlatform { + WindowsX64, + WindowsArm64, + LinuxX64, + LinuxArm64, + Unsupported, +}; + +std::optional BuildCudaEpManifest(CudaEpPlatform platform); + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc index 076256157..751750a0c 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc @@ -11,6 +11,7 @@ #include +#include #include #include #include @@ -39,10 +40,27 @@ namespace { constexpr int kArchiveHashRetries = 1; constexpr int kRawHashRetries = 0; +class ScopedDirectoryCleanup { + public: + explicit ScopedDirectoryCleanup(std::filesystem::path path) : path_(std::move(path)) {} + + ~ScopedDirectoryCleanup() { + std::error_code ec; + std::filesystem::remove_all(path_, ec); + } + + ScopedDirectoryCleanup(const ScopedDirectoryCleanup&) = delete; + ScopedDirectoryCleanup& operator=(const ScopedDirectoryCleanup&) = delete; + + void Release() { path_.clear(); } + + private: + std::filesystem::path path_; +}; + std::string GenerateUniqueId() { static thread_local std::mt19937_64 rng( - std::random_device{}() ^ - static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + std::random_device{}() ^ static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); return fmt::format("{:016x}", rng()); } @@ -62,8 +80,8 @@ std::optional ReadActiveMarker(const std::filesystem::path& active_ bool AtomicReplaceFile(const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec) { #ifdef _WIN32 - if (::MoveFileExW(from.wstring().c_str(), to.wstring().c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) { + if (::MoveFileExW(from.wstring().c_str(), to.wstring().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == + 0) { ec.assign(static_cast(::GetLastError()), std::system_category()); return false; } @@ -121,9 +139,8 @@ bool IsSafeId(std::string_view value) { return false; } - return std::all_of(value.begin(), value.end(), [](unsigned char ch) { - return std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.'; - }); + return std::all_of(value.begin(), value.end(), + [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.'; }); } bool IsSafeRelativePath(std::string_view value) { @@ -158,8 +175,7 @@ bool IsHttpsUrl(std::string_view value) { bool ValidateManifest(const EpBundleManifest& manifest, std::string_view ep_display_name, ILogger& logger) { if (!manifest.IsSupported()) { - logger.Log(LogLevel::Warning, - fmt::format("{}: bundle is disabled or incomplete", ep_display_name)); + logger.Log(LogLevel::Warning, fmt::format("{}: bundle is disabled or incomplete", ep_display_name)); return false; } @@ -178,19 +194,26 @@ bool ValidateManifest(const EpBundleManifest& manifest, std::string_view ep_disp } if (artifact.is_archive) { - if (!IsSha256(artifact.archive_sha256) || artifact.archive_max_bytes == 0 || - artifact.extracted_files.empty()) { + if (!IsSha256(artifact.archive_sha256) || artifact.archive_max_bytes == 0 || artifact.extracted_files.empty()) { logger.Log(LogLevel::Warning, fmt::format("{}: archive artifact metadata is incomplete", ep_display_name)); return false; } + std::unordered_set artifact_paths; for (const auto& file : artifact.extracted_files) { if (!IsSafeRelativePath(file.relative_path) || !IsSha256(file.sha256) || - !file_paths.insert(file.relative_path).second) { + !artifact_paths.insert(file.relative_path).second || !file_paths.insert(file.relative_path).second) { logger.Log(LogLevel::Warning, fmt::format("{}: archive file metadata is invalid", ep_display_name)); return false; } } + + for (const auto& ignored_path : artifact.ignored_archive_paths) { + if (!IsSafeRelativePath(ignored_path) || !artifact_paths.insert(ignored_path).second) { + logger.Log(LogLevel::Warning, fmt::format("{}: ignored archive path metadata is invalid", ep_display_name)); + return false; + } + } } else { if (!IsSafeRelativePath(artifact.raw_relative_path) || !IsSha256(artifact.raw_sha256) || artifact.raw_max_bytes == 0 || !file_paths.insert(artifact.raw_relative_path).second) { @@ -208,6 +231,56 @@ bool ValidateManifest(const EpBundleManifest& manifest, std::string_view ep_disp return true; } +bool EnsureManagedDirectory(const std::filesystem::path& path, std::string_view ep_display_name, ILogger& logger) { + std::error_code ec; + auto status = std::filesystem::symlink_status(path, ec); + if (ec && status.type() != std::filesystem::file_type::not_found) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to inspect managed directory '{}': {}", ep_display_name, + path.string(), ec.message())); + return false; + } + + if (status.type() == std::filesystem::file_type::not_found) { + ec.clear(); + std::filesystem::create_directory(path, ec); + if (ec) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to create managed directory '{}': {}", ep_display_name, + path.string(), ec.message())); + return false; + } + + status = std::filesystem::symlink_status(path, ec); + } + + if (ec || !std::filesystem::is_directory(status)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, path.string())); + return false; + } + + return true; +} + +bool ValidateManagedDirectories(const std::filesystem::path& bundles_dir, const std::filesystem::path& staging_root, + std::string_view ep_display_name, ILogger& logger) { + std::error_code ec; + const auto bundles_status = std::filesystem::symlink_status(bundles_dir, ec); + if (ec || !std::filesystem::is_directory(bundles_status)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, bundles_dir.string())); + return false; + } + + const auto staging_status = std::filesystem::symlink_status(staging_root, ec); + if (ec || !std::filesystem::is_directory(staging_status)) { + logger.Log(LogLevel::Warning, + fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, staging_root.string())); + return false; + } + + return true; +} + bool CollectRegularFiles(const std::filesystem::path& dir, std::unordered_set& files) { std::error_code ec; @@ -241,9 +314,8 @@ bool VerifyRegularFile(const std::filesystem::path& path, std::string_view expec bool VerifyArtifactFiles(const std::filesystem::path& bin_dir, const EpBundleArtifact& artifact) { if (artifact.is_archive) { - return std::all_of(artifact.extracted_files.begin(), artifact.extracted_files.end(), [&](const auto& file) { - return VerifyRegularFile(bin_dir / file.relative_path, file.sha256); - }); + return std::all_of(artifact.extracted_files.begin(), artifact.extracted_files.end(), + [&](const auto& file) { return VerifyRegularFile(bin_dir / file.relative_path, file.sha256); }); } return VerifyRegularFile(bin_dir / artifact.raw_relative_path, artifact.raw_sha256); @@ -265,8 +337,7 @@ bool CopyArtifactFiles(const std::filesystem::path& source_bin, const std::files const auto source = source_bin / relative_path; const auto destination = staging_bin / relative_path; std::filesystem::create_directories(destination.parent_path(), ec); - if (ec || !std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing, - ec)) { + if (ec || !std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing, ec)) { return false; } } @@ -288,53 +359,63 @@ bool VerifyBundleDir(const std::filesystem::path& bin_dir, const EpBundleManifes } if (!VerifyArtifactFiles(bin_dir, artifact)) { - logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' has invalid files for artifact '{}'", - ep_display_name, manifest.bundle_id, artifact.id)); + logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' has invalid files for artifact '{}'", ep_display_name, + manifest.bundle_id, artifact.id)); return false; } } std::unordered_set actual; if (!CollectRegularFiles(bin_dir, actual)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: bundle '{}' contains an unsupported filesystem entry", ep_display_name, - manifest.bundle_id)); + logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' contains an unsupported filesystem entry", + ep_display_name, manifest.bundle_id)); return false; } - if (actual.size() != expected.size()) { - logger.Log(LogLevel::Warning, - fmt::format("{}: bundle '{}' contains unexpected files ({} present, {} expected)", ep_display_name, - manifest.bundle_id, actual.size(), expected.size())); + if (actual != expected) { + logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' contains unexpected files ({} present, {} expected)", + ep_display_name, manifest.bundle_id, actual.size(), expected.size())); return false; } return true; } -void CleanupStaleGenerations(const std::filesystem::path& bundles_dir, const std::filesystem::path& staging_root, +bool CleanupStaleGenerations(const std::filesystem::path& bundles_dir, const std::filesystem::path& staging_root, const std::unordered_set& keep_ids, std::string_view ep_display_name, ILogger& logger) { - std::error_code ec; + if (!ValidateManagedDirectories(bundles_dir, staging_root, ep_display_name, logger)) { + return false; + } - if (std::filesystem::exists(staging_root, ec)) { - for (const auto& entry : std::filesystem::directory_iterator(staging_root, ec)) { - std::filesystem::remove_all(entry.path(), ec); + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(staging_root, ec)) { + std::filesystem::remove_all(entry.path(), ec); + if (ec) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to clean staging entry '{}': {}", ep_display_name, + entry.path().string(), ec.message())); + return false; } } - if (!std::filesystem::exists(bundles_dir, ec)) { - return; + if (ec) { + return false; } for (const auto& entry : std::filesystem::directory_iterator(bundles_dir, ec)) { if (keep_ids.count(entry.path().filename().string()) == 0) { - logger.Log(LogLevel::Debug, - fmt::format("{}: removing orphaned bundle generation '{}'", ep_display_name, - entry.path().filename().string())); + logger.Log(LogLevel::Debug, fmt::format("{}: removing orphaned bundle generation '{}'", ep_display_name, + entry.path().filename().string())); std::filesystem::remove_all(entry.path(), ec); + if (ec) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to remove orphaned bundle generation '{}': {}", + ep_display_name, entry.path().string(), ec.message())); + return false; + } } } + + return !ec; } void HardenFilePermissions(const std::filesystem::path& path) { @@ -345,6 +426,11 @@ void HardenFilePermissions(const std::filesystem::path& path) { std::filesystem::perm_options::replace, ec); } +bool ReportProgress(const IEpBootstrapper::ProgressCallback& progress_cb, std::string_view ep_display_name, + float percent) { + return !progress_cb || progress_cb(std::string(ep_display_name), percent); +} + bool DownloadWithHashRetry(const EpArtifactDownloadFn& download_fn, const std::string& url, const std::filesystem::path& destination, uint64_t max_bytes, const std::string& expected_sha256, int max_retries, std::string_view artifact_id, @@ -361,9 +447,14 @@ bool DownloadWithHashRetry(const EpArtifactDownloadFn& download_fn, const std::s } }; - if (!download_fn(url, destination, max_bytes, &cancel_flag, local_progress, logger)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: download failed for artifact '{}'", ep_display_name, artifact_id)); + const bool downloaded = download_fn(url, destination, max_bytes, &cancel_flag, local_progress, logger); + if (cancel_flag.load()) { + std::filesystem::remove(destination, ec); + return false; + } + + if (!downloaded) { + logger.Log(LogLevel::Warning, fmt::format("{}: download failed for artifact '{}'", ep_display_name, artifact_id)); return false; } @@ -373,8 +464,8 @@ bool DownloadWithHashRetry(const EpArtifactDownloadFn& download_fn, const std::s } logger.Log(LogLevel::Warning, - fmt::format("{}: hash mismatch for artifact '{}' (attempt {}/{}): got {}, expected {}", - ep_display_name, artifact_id, attempt + 1, max_retries + 1, hash, expected_sha256)); + fmt::format("{}: hash mismatch for artifact '{}' (attempt {}/{}): got {}, expected {}", ep_display_name, + artifact_id, attempt + 1, max_retries + 1, hash, expected_sha256)); } return false; @@ -384,7 +475,11 @@ bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBun const std::filesystem::path& staging_dir, const std::filesystem::path& staging_bin, float base_pct, float span_pct, std::string_view ep_display_name, const IEpBootstrapper::ProgressCallback& progress_cb, ILogger& logger) { - auto archive_path = staging_dir / (artifact.id + ".archive"); + const auto artifact_staging_dir = staging_dir / "artifacts" / artifact.id; + ScopedDirectoryCleanup artifact_cleanup(artifact_staging_dir); + const auto extraction_dir = artifact_staging_dir / "extracted"; + const auto archive_path = artifact_staging_dir / "download.archive"; + std::filesystem::create_directories(extraction_dir); if (!DownloadWithHashRetry(download_fn, artifact.url, archive_path, artifact.archive_max_bytes, artifact.archive_sha256, kArchiveHashRetries, artifact.id, ep_display_name, base_pct, @@ -392,7 +487,7 @@ bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBun return false; } - if (!ExtractZip(archive_path, staging_bin, logger)) { + if (!ExtractZip(archive_path, extraction_dir, logger)) { logger.Log(LogLevel::Warning, fmt::format("{}: extraction failed for artifact '{}'", ep_display_name, artifact.id)); return false; } @@ -400,12 +495,24 @@ bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBun std::error_code ec; std::filesystem::remove(archive_path, ec); + std::unordered_set expected_paths; + for (const auto& file : artifact.extracted_files) { + expected_paths.insert(file.relative_path); + } + expected_paths.insert(artifact.ignored_archive_paths.begin(), artifact.ignored_archive_paths.end()); + + std::unordered_set actual_paths; + if (!CollectRegularFiles(extraction_dir, actual_paths) || actual_paths != expected_paths) { + logger.Log(LogLevel::Warning, fmt::format("{}: artifact '{}' archive entries do not match its manifest", + ep_display_name, artifact.id)); + return false; + } + for (const auto& file : artifact.extracted_files) { - auto file_path = staging_bin / file.relative_path; + const auto file_path = extraction_dir / file.relative_path; if (!std::filesystem::is_regular_file(file_path, ec)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: artifact '{}' is missing expected extracted file '{}'", ep_display_name, - artifact.id, file.relative_path)); + logger.Log(LogLevel::Warning, fmt::format("{}: artifact '{}' is missing expected extracted file '{}'", + ep_display_name, artifact.id, file.relative_path)); return false; } @@ -417,10 +524,26 @@ bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBun return false; } - HardenFilePermissions(file_path); + const auto destination = staging_bin / file.relative_path; + std::filesystem::create_directories(destination.parent_path(), ec); + if (ec || + !std::filesystem::copy_file(file_path, destination, std::filesystem::copy_options::overwrite_existing, ec)) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to stage extracted file '{}' from artifact '{}'", + ep_display_name, file.relative_path, artifact.id)); + return false; + } + + HardenFilePermissions(destination); } - return true; + std::filesystem::remove_all(artifact_staging_dir, ec); + if (ec) { + return false; + } + + artifact_cleanup.Release(); + std::filesystem::remove(artifact_staging_dir.parent_path(), ec); + return !ec; } bool InstallRawArtifact(const EpArtifactDownloadFn& download_fn, const EpBundleArtifact& artifact, @@ -467,17 +590,24 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( std::filesystem::create_directories(root_dir_); auto lock = std::make_unique(root_dir_ / lock_file_name_); - auto bundles_dir = root_dir_ / "bundles"; - auto staging_root = root_dir_ / "staging"; - auto active_generation = ReadActiveMarker(root_dir_ / "active"); + const auto bundles_dir = root_dir_ / "bundles"; + const auto staging_root = root_dir_ / "staging"; + if (!EnsureManagedDirectory(bundles_dir, ep_display_name_, logger) || + !EnsureManagedDirectory(staging_root, ep_display_name_, logger)) { + return nullptr; + } + auto active_generation = ReadActiveMarker(root_dir_ / "active"); std::unordered_set keep_ids; if (active_generation.has_value() && IsSafeId(*active_generation)) { keep_ids.insert(*active_generation); } else { active_generation.reset(); } - CleanupStaleGenerations(bundles_dir, staging_root, keep_ids, ep_display_name_, logger); + + if (!CleanupStaleGenerations(bundles_dir, staging_root, keep_ids, ep_display_name_, logger)) { + return nullptr; + } std::filesystem::path active_bin; if (active_generation.has_value()) { @@ -488,20 +618,20 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( VerifyBundleDir(active_bin, manifest, ep_display_name_, logger)) { logger.Log(LogLevel::Information, fmt::format("{}: reusing verified bundle '{}'", ep_display_name_, manifest.bundle_id)); - if (progress_cb) { - progress_cb(ep_display_name_, 90.0f); + if (!ReportProgress(progress_cb, ep_display_name_, 90.0f)) { + return nullptr; } - return std::unique_ptr( - new EpInstallTransaction(std::move(lock), root_dir_, ep_display_name_, manifest, *active_generation, - active_bin)); + + return std::unique_ptr(new EpInstallTransaction( + std::move(lock), root_dir_, ep_display_name_, manifest, *active_generation, active_bin)); } auto staging_dir = staging_root / GenerateUniqueId(); + ScopedDirectoryCleanup staging_cleanup(staging_dir); auto staging_bin = staging_dir / "bin"; std::filesystem::create_directories(staging_bin); - logger.Log(LogLevel::Information, - fmt::format("{}: installing bundle '{}'", ep_display_name_, manifest.bundle_id)); + logger.Log(LogLevel::Information, fmt::format("{}: installing bundle '{}'", ep_display_name_, manifest.bundle_id)); const size_t artifact_count = manifest.artifacts.size(); for (size_t i = 0; i < artifact_count; ++i) { @@ -510,48 +640,47 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( const float span_pct = 80.0f / static_cast(artifact_count); bool ok = false; - if (policy == EpBundleInstallPolicy::ReuseVerified && !active_bin.empty() && - VerifyArtifactFiles(active_bin, artifact)) { + const bool reuse_artifact = policy == EpBundleInstallPolicy::ReuseVerified && !active_bin.empty() && + VerifyArtifactFiles(active_bin, artifact); + if (reuse_artifact) { ok = CopyArtifactFiles(active_bin, staging_bin, artifact); } else { - ok = artifact.is_archive - ? InstallArchiveArtifact(download_fn_, artifact, staging_dir, staging_bin, base_pct, span_pct, - ep_display_name_, progress_cb, logger) - : InstallRawArtifact(download_fn_, artifact, staging_bin, base_pct, span_pct, ep_display_name_, - progress_cb, logger); + ok = artifact.is_archive ? InstallArchiveArtifact(download_fn_, artifact, staging_dir, staging_bin, base_pct, + span_pct, ep_display_name_, progress_cb, logger) + : InstallRawArtifact(download_fn_, artifact, staging_bin, base_pct, span_pct, + ep_display_name_, progress_cb, logger); } if (!ok) { - std::error_code ec; - std::filesystem::remove_all(staging_dir, ec); + return nullptr; + } + + if (reuse_artifact && !ReportProgress(progress_cb, ep_display_name_, base_pct + span_pct)) { return nullptr; } } if (!VerifyBundleDir(staging_bin, manifest, ep_display_name_, logger)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: staged bundle '{}' failed verification before publish", ep_display_name_, - manifest.bundle_id)); - std::error_code ec; - std::filesystem::remove_all(staging_dir, ec); + logger.Log(LogLevel::Warning, fmt::format("{}: staged bundle '{}' failed verification before publish", + ep_display_name_, manifest.bundle_id)); return nullptr; } - std::filesystem::create_directories(bundles_dir); const auto generation_id = manifest.bundle_id + "-" + GenerateUniqueId(); - auto final_bundle_dir = bundles_dir / generation_id; + const auto final_bundle_dir = bundles_dir / generation_id; std::filesystem::rename(staging_dir, final_bundle_dir); + ScopedDirectoryCleanup final_cleanup(final_bundle_dir); - logger.Log(LogLevel::Information, - fmt::format("{}: installed bundle '{}'", ep_display_name_, manifest.bundle_id)); + logger.Log(LogLevel::Information, fmt::format("{}: installed bundle '{}'", ep_display_name_, manifest.bundle_id)); - if (progress_cb) { - progress_cb(ep_display_name_, 90.0f); + if (!ReportProgress(progress_cb, ep_display_name_, 90.0f)) { + return nullptr; } - return std::unique_ptr( - new EpInstallTransaction(std::move(lock), root_dir_, ep_display_name_, manifest, generation_id, - final_bundle_dir / "bin")); + auto transaction = std::unique_ptr(new EpInstallTransaction( + std::move(lock), root_dir_, ep_display_name_, manifest, generation_id, final_bundle_dir / "bin")); + final_cleanup.Release(); + return transaction; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("{}: install error: {}", ep_display_name_, e.what())); return nullptr; @@ -576,6 +705,12 @@ bool EpInstallTransaction::CommitActive(ILogger& logger) { } try { + const auto bundles_dir = root_dir_ / "bundles"; + const auto staging_root = root_dir_ / "staging"; + if (!ValidateManagedDirectories(bundles_dir, staging_root, ep_display_name_, logger)) { + return false; + } + if (!VerifyBundleDir(bin_dir_, manifest_, ep_display_name_, logger)) { logger.Log(LogLevel::Warning, fmt::format("{}: bundle '{}' failed re-verification before activation; active marker unchanged", @@ -588,8 +723,7 @@ bool EpInstallTransaction::CommitActive(ILogger& logger) { } committed_ = true; - CleanupStaleGenerations(root_dir_ / "bundles", root_dir_ / "staging", {generation_id_}, ep_display_name_, - logger); + CleanupStaleGenerations(bundles_dir, staging_root, {generation_id_}, ep_display_name_, logger); return true; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("{}: failed to commit active bundle: {}", ep_display_name_, e.what())); diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h index 3f1eb93ab..ce37105ea 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h @@ -20,6 +20,7 @@ struct EpBundleArtifact { std::string archive_sha256; std::vector extracted_files; + std::vector ignored_archive_paths; uint64_t archive_max_bytes = 0; std::string raw_relative_path; diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc index fa3bd490e..f4ba970f5 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -3,13 +3,10 @@ #include "ep_detection/ep_utils.h" #include "logger.h" -#include "util/sha256.h" #include "util/string_utils.h" #include -#include - #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include @@ -30,64 +27,6 @@ bool IsCoreRuntimeLibrary(const std::filesystem::path& filename) { } // namespace -bool VerifyEpArchive( - const std::filesystem::path& archive_path, - std::string_view expected_hash, - std::string_view ep_name, - ILogger& logger) { - if (!std::filesystem::exists(archive_path)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: archive missing: {}", ep_name, archive_path.string())); - return false; - } - - if (expected_hash.empty()) { - logger.Log(LogLevel::Warning, - fmt::format("{}: archive hash missing for {}", ep_name, archive_path.string())); - return false; - } - - auto hash = Sha256File(archive_path); - if (CompareCaseInsensitive(hash, std::string(expected_hash)) != 0) { - logger.Log(LogLevel::Warning, - fmt::format("{}: archive hash mismatch for {}: got {}, expected {}", - ep_name, - archive_path.filename().string(), - hash, - expected_hash)); - return false; - } - - return true; -} - -bool VerifyEpBinaries( - const std::filesystem::path& dir, - std::initializer_list> expected, - std::string_view ep_name, - ILogger& logger) { - - for (const auto& [filename, expected_hash] : expected) { - auto file_path = dir / filename; - - if (!std::filesystem::exists(file_path)) { - return false; - } - - auto hash = Sha256File(file_path); - - // Case-insensitive hex comparison - if (CompareCaseInsensitive(hash, std::string(expected_hash)) != 0) { - logger.Log(LogLevel::Warning, - fmt::format("{}: hash mismatch for {}: got {}, expected {}", - ep_name, filename, hash, expected_hash)); - return false; - } - } - - return true; -} - void PrependDirToProcessPath([[maybe_unused]] const std::filesystem::path& dir) { #ifdef _WIN32 DWORD len = GetEnvironmentVariableW(L"PATH", nullptr, 0); @@ -103,9 +42,8 @@ void PrependDirToProcessPath([[maybe_unused]] const std::filesystem::path& dir) #endif } -std::vector SelectEpBundleDependenciesToPreload( - const std::filesystem::path& bin_dir, - const EpBundleManifest& manifest) { +std::vector SelectEpBundleDependenciesToPreload(const std::filesystem::path& bin_dir, + const EpBundleManifest& manifest) { std::vector dependencies; const auto provider_filename = std::filesystem::path(manifest.provider_relative_path).filename().string(); @@ -126,24 +64,43 @@ std::vector SelectEpBundleDependenciesToPreload( return dependencies; } -bool LoadEpBundleDependencies( - [[maybe_unused]] const std::filesystem::path& bin_dir, - [[maybe_unused]] const EpBundleManifest& manifest, - [[maybe_unused]] std::string_view ep_name, - [[maybe_unused]] ILogger& logger) { +EpBundleDependencyOwner::~EpBundleDependencyOwner() { +#ifdef _WIN32 + for (auto it = handles_.rbegin(); it != handles_.rend(); ++it) { + FreeLibrary(static_cast(*it)); + } +#endif +} + +bool EpBundleDependencyOwner::Load([[maybe_unused]] const std::filesystem::path& bin_dir, + [[maybe_unused]] const EpBundleManifest& manifest, + [[maybe_unused]] std::string_view ep_name, [[maybe_unused]] ILogger& logger) { #ifdef _WIN32 - for (const auto& path : SelectEpBundleDependenciesToPreload(bin_dir, manifest)) { - if (!LoadLibraryExW(path.c_str(), nullptr, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)) { + const auto dependencies = SelectEpBundleDependenciesToPreload(bin_dir, manifest); + handles_.reserve(handles_.size() + dependencies.size()); + + std::vector loaded; + loaded.reserve(dependencies.size()); + + for (const auto& path : dependencies) { + auto* handle = + LoadLibraryExW(path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + if (handle == nullptr) { logger.Log(LogLevel::Warning, - fmt::format("{}: failed to load dependency '{}' ({})", - ep_name, path.string(), GetLastError())); + fmt::format("{}: failed to load dependency '{}' ({})", ep_name, path.string(), GetLastError())); + + for (auto it = loaded.rbegin(); it != loaded.rend(); ++it) { + FreeLibrary(static_cast(*it)); + } + return false; } + + loaded.push_back(handle); } + + handles_.insert(handles_.end(), loaded.begin(), loaded.end()); #endif - // Preloading is a Windows-specific concern (LoadLibraryExW search-path flags); other platforms rely - // on RPATH/PATH-style resolution and don't need this step. return true; } diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h index 8fcb27ed8..abbe77f0f 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -5,41 +5,13 @@ #include "ep_detection/ep_bundle_manifest.h" #include -#include #include -#include #include namespace fl { class ILogger; -/// Verify an EP archive file matches the expected SHA-256 hash. -/// -/// @param archive_path Archive file path to verify. -/// @param expected_hash Expected SHA-256 hash for @p archive_path. -/// @param ep_name EP name used in warning log messages. -/// @param logger Logger for diagnostic output. -/// @return true if archive exists and hash matches; false otherwise. -bool VerifyEpArchive( - const std::filesystem::path& archive_path, - std::string_view expected_hash, - std::string_view ep_name, - ILogger& logger); - -/// Verify a set of binaries in @p dir all exist and match their expected SHA-256 hashes. -/// -/// @param dir Directory containing the extracted EP binaries. -/// @param expected List of (filename, expected_sha256_hex) pairs. -/// @param ep_name EP name used in warning log messages (e.g. "CUDA EP"). -/// @param logger Logger for diagnostic output. -/// @return true if every file exists and its hash matches; false otherwise. -bool VerifyEpBinaries( - const std::filesystem::path& dir, - std::initializer_list> expected, - std::string_view ep_name, - ILogger& logger); - /// Prepend @p dir to the process `PATH` environment variable for the lifetime of the process. /// /// EP provider libraries (CUDA, WebGPU) delay-load sibling dependency DLLs from their own directory, @@ -60,29 +32,22 @@ void PrependDirToProcessPath(const std::filesystem::path& dir); /// @param bin_dir Directory containing the extracted bundle files. /// @param manifest Bundle manifest describing the extracted artifacts. /// @return Absolute paths of the DLLs that should be preloaded, in manifest order. -std::vector SelectEpBundleDependenciesToPreload( - const std::filesystem::path& bin_dir, - const EpBundleManifest& manifest); +std::vector SelectEpBundleDependenciesToPreload(const std::filesystem::path& bin_dir, + const EpBundleManifest& manifest); -/// Preload the non-provider, non-core-runtime DLLs declared by @p manifest from @p bin_dir. -/// -/// EP provider libraries (CUDA, WebGPU) can implicitly or delay-load sibling dependency DLLs, and -/// `RegisterExecutionProviderLibrary` loads the provider DLL eagerly. Preloading those dependencies by -/// absolute path with `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32` ensures they -/// resolve correctly regardless of the process `PATH`, before the provider DLL is registered. -/// -/// This is a no-op that returns true on non-Windows platforms. -/// -/// @param bin_dir Directory containing the extracted bundle files. -/// @param manifest Bundle manifest describing the extracted artifacts. -/// @param ep_name EP name used in warning log messages (e.g. "CUDA EP"). -/// @param logger Logger for diagnostic output. -/// @return true if every selected dependency loaded successfully (or none needed loading); false -/// otherwise. -bool LoadEpBundleDependencies( - const std::filesystem::path& bin_dir, - const EpBundleManifest& manifest, - std::string_view ep_name, - ILogger& logger); +class EpBundleDependencyOwner { + public: + EpBundleDependencyOwner() = default; + ~EpBundleDependencyOwner(); + + EpBundleDependencyOwner(const EpBundleDependencyOwner&) = delete; + EpBundleDependencyOwner& operator=(const EpBundleDependencyOwner&) = delete; + + bool Load(const std::filesystem::path& bin_dir, const EpBundleManifest& manifest, std::string_view ep_name, + ILogger& logger); + + private: + std::vector handles_; +}; } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc index c1dab2e1a..8aca8957b 100644 --- a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc +++ b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc @@ -4,11 +4,11 @@ #include -#ifdef _WIN32 +#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT) #define WIN32_LEAN_AND_MEAN #include #include -#else +#elif !defined(_WIN32) #include #endif @@ -25,7 +25,7 @@ using NvmlDeviceGetCountFn = int (*)(unsigned int*); using NvmlDeviceGetHandleByIndexFn = int (*)(unsigned int, NvmlDevice*); using NvmlDeviceGetCudaComputeCapabilityFn = int (*)(NvmlDevice, int*, int*); -#ifdef _WIN32 +#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT) using LibraryHandle = HMODULE; constexpr LibraryHandle kNullLibrary = nullptr; @@ -58,9 +58,7 @@ LibraryHandle LoadNvmlLibrary() { #endif } -void* GetSymbol(LibraryHandle lib, const char* name) { - return reinterpret_cast(GetProcAddress(lib, name)); -} +void* GetSymbol(LibraryHandle lib, const char* name) { return reinterpret_cast(GetProcAddress(lib, name)); } void UnloadLibrary(LibraryHandle lib) { if (lib) { @@ -68,18 +66,25 @@ void UnloadLibrary(LibraryHandle lib) { } } +#elif defined(_WIN32) + +using LibraryHandle = void*; +constexpr LibraryHandle kNullLibrary = nullptr; + +LibraryHandle LoadNvmlLibrary() { return nullptr; } + +void* GetSymbol(LibraryHandle /*lib*/, const char* /*name*/) { return nullptr; } + +void UnloadLibrary(LibraryHandle /*lib*/) {} + #else using LibraryHandle = void*; constexpr LibraryHandle kNullLibrary = nullptr; -LibraryHandle LoadNvmlLibrary() { - return dlopen("libnvidia-ml.so.1", RTLD_NOW | RTLD_LOCAL); -} +LibraryHandle LoadNvmlLibrary() { return dlopen("libnvidia-ml.so.1", RTLD_NOW | RTLD_LOCAL); } -void* GetSymbol(LibraryHandle lib, const char* name) { - return dlsym(lib, name); -} +void* GetSymbol(LibraryHandle lib, const char* name) { return dlsym(lib, name); } void UnloadLibrary(LibraryHandle lib) { if (lib) { @@ -100,10 +105,9 @@ class NvmlLibrary { init_ = reinterpret_cast(GetSymbol(lib_, "nvmlInit_v2")); shutdown_ = reinterpret_cast(GetSymbol(lib_, "nvmlShutdown")); get_count_ = reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetCount_v2")); - get_handle_ = - reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetHandleByIndex_v2")); - get_compute_cap_ = reinterpret_cast( - GetSymbol(lib_, "nvmlDeviceGetCudaComputeCapability")); + get_handle_ = reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetHandleByIndex_v2")); + get_compute_cap_ = + reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetCudaComputeCapability")); if (!init_ || !shutdown_ || !get_count_ || !get_handle_ || !get_compute_cap_) { UnloadLibrary(lib_); @@ -165,8 +169,7 @@ class NvmlLibrary { } // namespace -bool HasQualifyingComputeCapability(const std::vector>& capabilities, - int min_major, +bool HasQualifyingComputeCapability(const std::vector>& capabilities, int min_major, int min_minor) { for (const auto& [major, minor] : capabilities) { if (major > min_major || (major == min_major && minor >= min_minor)) { diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index de7229cb4..06c613a8a 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -43,7 +43,6 @@ constexpr const char* kProviderSha256 = "8FAC874A60F32F0127C74CB7DEF915807FCC8A6 constexpr const char* kRegistrationName = "Foundry.WebGPU"; constexpr const char* kWebGpuProviderOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; -constexpr const char* kVersionSha256 = "4CB81DA21A42BC8A1DE985A2C6C7DFEE3F634576B0C8C7FA0990FB027F1BB082"; constexpr uint64_t kArchiveMaxBytes = 64ULL * 1024 * 1024; std::optional BuildWebGpuManifest() { @@ -61,8 +60,8 @@ std::optional BuildWebGpuManifest() { {.relative_path = "dxcompiler.dll", .sha256 = kDxCompilerSha256}, {.relative_path = "dxil.dll", .sha256 = kDxilSha256}, {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = kProviderSha256}, - {.relative_path = "version.json", .sha256 = kVersionSha256}, }, + .ignored_archive_paths = {"version.json"}, .archive_max_bytes = kArchiveMaxBytes, .raw_relative_path = "", .raw_sha256 = "", @@ -81,8 +80,8 @@ std::optional BuildWebGpuManifest() { .extracted_files = { {.relative_path = "libonnxruntime_providers_webgpu.dylib", .sha256 = kProviderSha256}, - {.relative_path = "version.json", .sha256 = kVersionSha256}, }, + .ignored_archive_paths = {"version.json"}, .archive_max_bytes = kArchiveMaxBytes, .raw_relative_path = "", .raw_sha256 = "", @@ -99,20 +98,13 @@ std::optional BuildWebGpuManifest() { namespace fl { WebGpuEpBootstrapper::WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) - : register_ep_(std::move(register_ep)), - installer_(std::filesystem::path(root_dir), kLockFileName, "WebGPU EP") {} + : register_ep_(std::move(register_ep)), installer_(std::filesystem::path(root_dir), kLockFileName, "WebGPU EP") {} -const std::string& WebGpuEpBootstrapper::Name() const { - return name_; -} +const std::string& WebGpuEpBootstrapper::Name() const { return name_; } -bool WebGpuEpBootstrapper::IsRegistered() const { - return registered_; -} +bool WebGpuEpBootstrapper::IsRegistered() const { return registered_; } -bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, - const ProgressCallback& progress_cb, - ILogger& logger) { +bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) { if (registered_ && !force) { if (progress_cb) { progress_cb(name_, 100.0f); @@ -133,24 +125,20 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, std::filesystem::path provider_path = std::filesystem::absolute(*override_path); if (!std::filesystem::exists(provider_path)) { - logger.Log(LogLevel::Warning, - fmt::format("WebGPU EP: {} set but file does not exist ({})", - kWebGpuProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: {} set but file does not exist ({})", + kWebGpuProviderOverrideEnv, provider_path.string())); return false; } - if (progress_cb) { - progress_cb(name_, 90.0f); + if (progress_cb && !progress_cb(name_, 90.0f)) { + return false; } - // Prepend the override directory to PATH so sibling dependency DLLs are discoverable, - // matching the normal install path. The WebGPU EP may delay-load dependencies. PrependDirToProcessPath(provider_path.parent_path()); if (!register_ep_(kRegistrationName, provider_path)) { - logger.Log(LogLevel::Warning, - fmt::format("WebGPU EP: ORT registration failed for override {}={}", - kWebGpuProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: ORT registration failed for override {}={}", + kWebGpuProviderOverrideEnv, provider_path.string())); return false; } @@ -160,9 +148,8 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, progress_cb(name_, 100.0f); } - logger.Log(LogLevel::Information, - fmt::format("WebGPU EP: ready (override_env={} install_path={})", - kWebGpuProviderOverrideEnv, provider_path.string())); + logger.Log(LogLevel::Information, fmt::format("WebGPU EP: ready (override_env={} install_path={})", + kWebGpuProviderOverrideEnv, provider_path.string())); return true; } @@ -172,22 +159,24 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, return false; } - const auto install_policy = - force ? EpBundleInstallPolicy::ForceDownload : EpBundleInstallPolicy::ReuseVerified; + const auto install_policy = force ? EpBundleInstallPolicy::ForceDownload : EpBundleInstallPolicy::ReuseVerified; auto txn = installer_.EnsureInstalled(*manifest, progress_cb, logger, install_policy); if (!txn) { return false; } - auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + if (!txn->CommitActive(logger)) { + logger.Log(LogLevel::Warning, "WebGPU EP: failed to publish active bundle marker"); + return false; + } + const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - // The provider delay-loads sibling DirectX compiler binaries after registration; keep PATH - // primed as a fallback in addition to the explicit preload below. + // The provider can delay-load sibling DirectX compiler binaries after registration. PrependDirToProcessPath(txn->bin_dir()); #endif - if (!LoadEpBundleDependencies(txn->bin_dir(), *manifest, "WebGPU EP", logger)) { + if (!dependency_owner_.Load(txn->bin_dir(), *manifest, "WebGPU EP", logger)) { return false; } @@ -198,16 +187,11 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, registered_ = true; - if (!txn->CommitActive(logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: failed to publish active bundle marker"); - } - if (progress_cb) { progress_cb(name_, 100.0f); } - logger.Log(LogLevel::Information, - fmt::format("WebGPU EP: ready (install_path={})", txn->bin_dir().string())); + logger.Log(LogLevel::Information, fmt::format("WebGPU EP: ready (install_path={})", txn->bin_dir().string())); return true; } catch (const std::exception& e) { logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: error: {}", e.what())); @@ -216,8 +200,7 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, } bool WebGpuEpBootstrapper::IsSupportedPlatform() { -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__APPLE__) && defined(__aarch64__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || (defined(__APPLE__) && defined(__aarch64__)) return true; #else return false; diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index 3fd2ed799..c89841bba 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -5,6 +5,7 @@ #include "ep_detection/ep_bootstrapper.h" #include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" +#include "ep_detection/ep_utils.h" #include @@ -28,9 +29,7 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { const std::string& Name() const override; bool IsRegistered() const override; - bool DownloadAndRegister(bool force, - const ProgressCallback& progress_cb, - ILogger& logger) override; + bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) override; /// Whether Foundry Local publishes a WebGPU EP bundle for this platform. static bool IsSupportedPlatform(); @@ -41,6 +40,7 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { int attempts_ = 0; EpRegistrationCallback register_ep_; EpBundleInstaller installer_; + EpBundleDependencyOwner dependency_owner_; }; } // namespace fl diff --git a/sdk_v2/cpp/src/http/http_download.cc b/sdk_v2/cpp/src/http/http_download.cc index 772893a58..a1e680193 100644 --- a/sdk_v2/cpp/src/http/http_download.cc +++ b/sdk_v2/cpp/src/http/http_download.cc @@ -17,8 +17,10 @@ #include #endif +#include #include #include +#include namespace fl { @@ -31,12 +33,30 @@ std::string RedactUrlForLog(const std::string& url) { } // namespace -bool HttpDownloadFile(const std::string& url, - const std::filesystem::path& destination, - const std::string& user_agent, - std::atomic* cancel_flag, - std::function progress_cb, - ILogger& logger, +std::optional ParseContentLengthHeader(std::string_view value) { + constexpr auto is_http_whitespace = [](char ch) { return ch == ' ' || ch == '\t'; }; + while (!value.empty() && is_http_whitespace(value.front())) { + value.remove_prefix(1); + } + while (!value.empty() && is_http_whitespace(value.back())) { + value.remove_suffix(1); + } + + if (value.empty()) { + return std::nullopt; + } + + int64_t parsed = 0; + const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), parsed); + if (error != std::errc{} || end != value.data() + value.size() || parsed < 0) { + return std::nullopt; + } + + return parsed; +} + +bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, + std::atomic* cancel_flag, std::function progress_cb, ILogger& logger, int64_t max_bytes) { using namespace Azure::Core; using namespace Azure::Core::Http; @@ -56,8 +76,8 @@ bool HttpDownloadFile(const std::string& url, request.SetHeader("User-Agent", user_agent); // Long timeout for large downloads (30 minutes) - Context context = Context{}.WithDeadline( - Azure::DateTime(std::chrono::system_clock::now() + std::chrono::minutes(30))); + Context context = + Context{}.WithDeadline(Azure::DateTime(std::chrono::system_clock::now() + std::chrono::minutes(30))); std::unique_ptr response; const auto log_url = RedactUrlForLog(url); @@ -91,34 +111,26 @@ bool HttpDownloadFile(const std::string& url, int64_t content_length = -1; auto cl_header = response->GetHeaders().find("content-length"); if (cl_header != response->GetHeaders().end()) { - try { - content_length = std::stoll(cl_header->second); - } catch (const std::exception& ex) { - logger.Log(LogLevel::Warning, - MakeString("HTTP download: invalid Content-Length header for ", log_url, - " (\"", cl_header->second, "\"): ", ex.what())); + const auto parsed_content_length = ParseContentLengthHeader(cl_header->second); + if (!parsed_content_length.has_value()) { + logger.Log(LogLevel::Warning, MakeString("HTTP download: invalid Content-Length header for ", log_url, " (\"", + cl_header->second, "\")")); return false; } - if (content_length < 0) { - logger.Log(LogLevel::Warning, - MakeString("HTTP download: negative Content-Length for ", log_url, - " (\"", cl_header->second, "\")")); - return false; - } + content_length = *parsed_content_length; if (max_bytes >= 0 && content_length > max_bytes) { logger.Log(LogLevel::Warning, - MakeString("HTTP download: Content-Length ", content_length, " for ", log_url, - " exceeds the ", max_bytes, "-byte cap; refusing before reading any body")); + MakeString("HTTP download: Content-Length ", content_length, " for ", log_url, " exceeds the ", + max_bytes, "-byte cap; refusing before reading any body")); return false; } } std::ofstream out(destination, std::ios::binary); if (!out) { - logger.Log(LogLevel::Warning, - MakeString("HTTP download: failed to open output file ", destination.string())); + logger.Log(LogLevel::Warning, MakeString("HTTP download: failed to open output file ", destination.string())); return false; } @@ -146,8 +158,7 @@ bool HttpDownloadFile(const std::string& url, out.write(reinterpret_cast(buffer), static_cast(bytes_read)); if (!out) { - logger.Log(LogLevel::Warning, - MakeString("HTTP download: failed writing ", destination.string())); + logger.Log(LogLevel::Warning, MakeString("HTTP download: failed writing ", destination.string())); out.close(); remove_destination(); return false; @@ -186,9 +197,8 @@ bool HttpDownloadFile(const std::string& url, // detect truncated transfer. If the server promised a content length and // we received fewer bytes, surface the error rather than reporting success. if (content_length > 0 && bytes_downloaded < content_length) { - logger.Log(LogLevel::Warning, - MakeString("HTTP download truncated for ", log_url, ": got ", - bytes_downloaded, " of ", content_length, " bytes")); + logger.Log(LogLevel::Warning, MakeString("HTTP download truncated for ", log_url, ": got ", bytes_downloaded, + " of ", content_length, " bytes")); remove_destination(); return false; } diff --git a/sdk_v2/cpp/src/http/http_download.h b/sdk_v2/cpp/src/http/http_download.h index a5e2835ab..ea91c3f69 100644 --- a/sdk_v2/cpp/src/http/http_download.h +++ b/sdk_v2/cpp/src/http/http_download.h @@ -6,12 +6,16 @@ #include #include #include +#include #include +#include namespace fl { class ILogger; +std::optional ParseContentLengthHeader(std::string_view value); + /// Download a file from an HTTP(S) URL to a local path. /// Supports progress reporting, cancellation, and an optional size cap. /// @param url The URL to download from. @@ -24,12 +28,8 @@ class ILogger; /// if the body exceeds it regardless of what Content-Length promised (defends /// against a missing/incorrect header on chunked transfers). -1 means no cap. /// @return true on success, false on failure. -bool HttpDownloadFile(const std::string& url, - const std::filesystem::path& destination, - const std::string& user_agent, - std::atomic* cancel_flag, - std::function progress_cb, - ILogger& logger, +bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, + std::atomic* cancel_flag, std::function progress_cb, ILogger& logger, int64_t max_bytes = -1); } // namespace fl diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index 19b61b9ee..0443ff742 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -87,12 +87,8 @@ LogLevel MapOrtLogLevel(OrtLoggingLevel severity) { } } -void ORT_API_CALL OrtLogCallback(void* /*logger_param*/, - OrtLoggingLevel severity, - const char* category, - const char* logid, - const char* code_location, - const char* message) { +void ORT_API_CALL OrtLogCallback(void* /*logger_param*/, OrtLoggingLevel severity, const char* category, + const char* logid, const char* code_location, const char* message) { auto* logger = s_ort_logger.load(std::memory_order_acquire); if (logger == nullptr) { return; @@ -160,14 +156,18 @@ void SetOgaLogCallback(ILogger* logger) { return; } + if (logger == nullptr) { + OgaDestroyResult(result); + s_oga_logger.store(nullptr, std::memory_order_release); + return; + } + const char* err = OgaResultGetError(result); std::string err_msg = err ? err : "unknown"; OgaDestroyResult(result); - if (logger != nullptr) { - logger->Log(LogLevel::Warning, "Failed to set GenAI log callback: " + err_msg); - s_oga_logger.store(nullptr, std::memory_order_release); - } + logger->Log(LogLevel::Warning, "Failed to set GenAI log callback: " + err_msg); + s_oga_logger.store(nullptr, std::memory_order_release); } } // namespace @@ -175,8 +175,7 @@ void SetOgaLogCallback(ILogger* logger) { std::mutex Manager::s_mutex_; std::unique_ptr Manager::s_instance_; -Manager::Manager(const Configuration& config) - : config_(config) { +Manager::Manager(const Configuration& config) : config_(config) { config_.Validate(); const bool genai_verbose_logging = IsGenAIVerboseLoggingEnabled(); @@ -217,16 +216,14 @@ Manager::Manager(const Configuration& config) LogRuntimeVersions(*logger_); - EpRegistrationCallback register_ep = [this, &log = *logger_]( - const std::string& registration_name, - const std::filesystem::path& library_path) -> bool { - OrtStatus* status = ort_api_->RegisterExecutionProviderLibrary( - ort_env_, registration_name.c_str(), library_path.c_str()); + EpRegistrationCallback register_ep = [this, &log = *logger_](const std::string& registration_name, + const std::filesystem::path& library_path) -> bool { + OrtStatus* status = + ort_api_->RegisterExecutionProviderLibrary(ort_env_, registration_name.c_str(), library_path.c_str()); if (status != nullptr) { const char* msg = ort_api_->GetErrorMessage(status); - log.Log(LogLevel::Warning, - std::string("EP registration: RegisterExecutionProviderLibrary failed for '") + - registration_name + "': " + (msg ? msg : "unknown")); + log.Log(LogLevel::Warning, std::string("EP registration: RegisterExecutionProviderLibrary failed for '") + + registration_name + "': " + (msg ? msg : "unknown")); ort_api_->ReleaseStatus(status); return false; } @@ -234,10 +231,9 @@ Manager::Manager(const Configuration& config) registered_ep_libraries_.push_back(registration_name); auto version = GetEpVersion(*ort_api_, *ort_env_, registration_name); - log.Log(LogLevel::Information, - std::string("EP registration: '") + registration_name + - "' registered successfully (library=" + library_path.string() + - ", version=" + version + ")"); + log.Log(LogLevel::Information, std::string("EP registration: '") + registration_name + + "' registered successfully (library=" + library_path.string() + + ", version=" + version + ")"); return true; }; @@ -246,8 +242,7 @@ Manager::Manager(const Configuration& config) // Detected once and reused below for the WinML catalog skip-list and CUDA bootstrapper. // Avoid probing NVML on platforms where Foundry Local does not publish a CUDA bundle. - const bool has_nvidia_gpu = - CudaEpBootstrapper::IsSupportedPlatform() && CudaEpBootstrapper::HasNvidiaGpu(); + const bool has_nvidia_gpu = CudaEpBootstrapper::IsSupportedPlatform() && CudaEpBootstrapper::HasNvidiaGpu(); #if FOUNDRY_LOCAL_HAS_EP_CATALOG // WinML EPs — enumerate from the OS EP catalog (Windows 10 19H1+ reg-free runtime). @@ -293,43 +288,41 @@ Manager::Manager(const Configuration& config) // Accepts case-insensitive true/1/yes. const bool disable_region_fallback = IsAdditionalOptionEnabled(config_, "DisableRegionFallback"); - download_manager_ = std::make_unique( - *config_.model_cache_dir, - config_.catalog_region.value_or("auto"), - download_concurrency, - *logger_, - disable_region_fallback); + download_manager_ = + std::make_unique(*config_.model_cache_dir, config_.catalog_region.value_or("auto"), + download_concurrency, *logger_, disable_region_fallback); model_load_manager_ = std::make_unique(*ep_detector_, *logger_); session_manager_ = std::make_unique(*logger_); telemetry_ = std::make_unique(config_.app_name, *logger_); catalog_ = std::make_unique( - config_.catalog_urls, - download_manager_->GetCacheDirectory(), - [this](ModelInfo info, std::string local_path) { - return CreateModel(std::move(info), std::move(local_path)); - }, - *ep_detector_, *logger_, - config_.external_service_url.has_value(), - config_.catalog_region.value_or("auto"), + config_.catalog_urls, download_manager_->GetCacheDirectory(), + [this](ModelInfo info, std::string local_path) { return CreateModel(std::move(info), std::move(local_path)); }, + *ep_detector_, *logger_, config_.external_service_url.has_value(), config_.catalog_region.value_or("auto"), disable_region_fallback); } Manager::~Manager() { - // Signal subsystems to drain before tearing down infrastructure + const auto safe_log = [this](LogLevel level, std::string_view message) noexcept { + try { + if (logger_ != nullptr) { + logger_->Log(level, message); + } + } catch (...) { + } + }; + try { Shutdown(); } catch (const std::exception& e) { - logger_->Log(LogLevel::Error, - std::string("Exception while shutting down Manager subsystems during destruction: ") + e.what()); + try { + safe_log(LogLevel::Error, + std::string("Exception while shutting down Manager subsystems during destruction: ") + e.what()); + } catch (...) { + } } catch (...) { - // Suppress exceptions during destruction - logger_->Log(LogLevel::Error, "Unknown exception while shutting down Manager subsystems during destruction."); + safe_log(LogLevel::Error, "Unknown exception while shutting down Manager subsystems during destruction."); } - // Tear down members that hold OrtEnv references / live ORT sessions before - // we unregister EPs and release the env. C++ would destroy these in reverse - // declaration order after this function returns, but the env release below - // requires they be gone *now*. #ifdef FOUNDRY_LOCAL_HAS_WEB_SERVICE web_service_.reset(); #endif @@ -338,35 +331,36 @@ Manager::~Manager() { download_manager_.reset(); catalog_.reset(); telemetry_.reset(); - ep_detector_.reset(); - // GenAI owns process-global ORT state and the CUDA add-on handle. Tear it down after every - // GenAI model/session is gone, but before unregistering provider libraries from our OrtEnv. OgaShutdown(); - // Unregister EPs we registered, then drop our OrtEnv refcount. Best-effort: - // log failures but don't throw from a destructor. if (ort_api_ != nullptr && ort_env_ != nullptr) { - for (const auto& name : registered_ep_libraries_) { + for (auto it = registered_ep_libraries_.rbegin(); it != registered_ep_libraries_.rend(); ++it) { + const auto& name = *it; OrtStatus* status = ort_api_->UnregisterExecutionProviderLibrary(ort_env_, name.c_str()); if (status != nullptr) { const char* msg = ort_api_->GetErrorMessage(status); - logger_->Log(LogLevel::Warning, - std::string("EP unregister: UnregisterExecutionProviderLibrary failed for '") + - name + "': " + (msg ? msg : "unknown")); + try { + safe_log(LogLevel::Warning, std::string("EP unregister: UnregisterExecutionProviderLibrary failed for '") + + name + "': " + (msg ? msg : "unknown")); + } catch (...) { + } ort_api_->ReleaseStatus(status); } } + ep_detector_.reset(); ort_api_->ReleaseEnv(ort_env_); ort_env_ = nullptr; + } else { + ep_detector_.reset(); } if (s_oga_logger.load(std::memory_order_acquire) != nullptr) { SetOgaLogCallback(nullptr); } - logger_->Log(LogLevel::Information, "Manager is being disposed."); + safe_log(LogLevel::Information, "Manager is being disposed."); // ORT may still emit late teardown logs from internal static cleanup after Manager destruction // due to GenAI keeping the OrtEnv alive until the process exits. @@ -419,9 +413,7 @@ void Manager::Destroy() { s_instance_.reset(); } -ICatalog& Manager::GetCatalog() { - return *catalog_; -} +ICatalog& Manager::GetCatalog() { return *catalog_; } void Manager::StartWebService() { if (web_service_running_) { @@ -437,8 +429,7 @@ void Manager::StartWebService() { #ifdef FOUNDRY_LOCAL_HAS_WEB_SERVICE web_service_ = std::make_unique(*catalog_, *logger_, *config_.model_cache_dir, *model_load_manager_, - *session_manager_, *telemetry_, - [this]() { Shutdown(); }); + *session_manager_, *telemetry_, [this]() { Shutdown(); }); auto endpoints = config_.web_service_endpoints; if (endpoints.empty()) { @@ -507,52 +498,30 @@ void Manager::Shutdown() { model_load_manager_->UnloadAll(); } -bool Manager::IsShutdownRequested() const { - return shutdown_requested_.load(); -} +bool Manager::IsShutdownRequested() const { return shutdown_requested_.load(); } -const Configuration& Manager::GetConfiguration() const { - return config_; -} +const Configuration& Manager::GetConfiguration() const { return config_; } Model Manager::CreateModel(ModelInfo info, std::string local_path) { - return Model::FromModelInfo(std::move(info), - std::move(local_path), - *download_manager_, - *model_load_manager_); + return Model::FromModelInfo(std::move(info), std::move(local_path), *download_manager_, *model_load_manager_); } -DownloadManager& Manager::GetDownloadManager() { - return *download_manager_; -} +DownloadManager& Manager::GetDownloadManager() { return *download_manager_; } -ModelLoadManager& Manager::GetModelLoadManager() { - return *model_load_manager_; -} +ModelLoadManager& Manager::GetModelLoadManager() { return *model_load_manager_; } -SessionManager& Manager::GetSessionManager() { - return *session_manager_; -} +SessionManager& Manager::GetSessionManager() { return *session_manager_; } -ILogger& Manager::GetLogger() { - return *logger_; -} +ILogger& Manager::GetLogger() { return *logger_; } -ITelemetry& Manager::GetTelemetry() { - return *telemetry_; -} +ITelemetry& Manager::GetTelemetry() { return *telemetry_; } -const IEpDetector& Manager::GetEpDetector() const { - return *ep_detector_; -} +const IEpDetector& Manager::GetEpDetector() const { return *ep_detector_; } -IEpDetector& Manager::GetEpDetector() { - return *ep_detector_; -} +IEpDetector& Manager::GetEpDetector() { return *ep_detector_; } -EpDownloadResult Manager::DownloadAndRegisterEps( - const std::vector* names, - const IEpBootstrapper::ProgressCallback& progress_cb) { +EpDownloadResult Manager::DownloadAndRegisterEps(const std::vector* names, + const IEpBootstrapper::ProgressCallback& progress_cb) { auto result = ep_detector_->DownloadAndRegisterEps(names, progress_cb); // EP registration changes which device/EP filters the catalog uses. diff --git a/sdk_v2/cpp/src/manager.h b/sdk_v2/cpp/src/manager.h index cbfce33f5..bd2020b5f 100644 --- a/sdk_v2/cpp/src/manager.h +++ b/sdk_v2/cpp/src/manager.h @@ -66,9 +66,8 @@ class Manager { /// so subsequent catalog queries reflect the new device/EP availability. /// This is the preferred entry point — going through GetEpDetector() directly /// will not invalidate the catalog. - EpDownloadResult DownloadAndRegisterEps( - const std::vector* names, - const IEpBootstrapper::ProgressCallback& progress_cb); + EpDownloadResult DownloadAndRegisterEps(const std::vector* names, + const IEpBootstrapper::ProgressCallback& progress_cb); /// Get the model load manager (for loading/unloading ORT GenAI models). ModelLoadManager& GetModelLoadManager(); @@ -120,15 +119,14 @@ class Manager { // released manually in ~Manager() after all // consumers and GenAI globals are gone. // logger_ — everything logs through this, destroyed last - // ep_detector_ — detects HW acceleration; holds OrtEnv& (must - // outlive ort_env_ release in ~Manager()) + // ep_detector_ — owns EP bootstrappers and dependency handles; + // reset after provider unregistration and before OrtEnv release // telemetry_ — used throughout - // catalog_ — owns all Model instances. used by download_manager, model_load_manager, and web service - // download_manager_ — uses ModelInfo owned by catalog - // model_load_manager_ — holds loaded model state referencing catalog models - // session_manager_ — tracks all active sessions. destroyed after web service, before models - // shutdown_requested_ — atomic flag checked by subsystems and the host process - // web service members — use catalog, model_load_manager, session_manager, telemetry, logger + // catalog_ — owns all Model instances. used by download_manager, model_load_manager, and web + // service download_manager_ — uses ModelInfo owned by catalog model_load_manager_ — holds loaded model + // state referencing catalog models session_manager_ — tracks all active sessions. destroyed after web + // service, before models shutdown_requested_ — atomic flag checked by subsystems and the host process web + // service members — use catalog, model_load_manager, session_manager, telemetry, logger // Configuration config_; const OrtApi* ort_api_ = nullptr; diff --git a/sdk_v2/cpp/src/util/zip_extract.cc b/sdk_v2/cpp/src/util/zip_extract.cc index f432391f1..027c22dbd 100644 --- a/sdk_v2/cpp/src/util/zip_extract.cc +++ b/sdk_v2/cpp/src/util/zip_extract.cc @@ -26,8 +26,7 @@ bool IsSafeArchiveEntry(std::string_view entry) { return true; } - if (entry.front() == '/' || entry.front() == '\\' || - entry.find(':') != std::string_view::npos) { + if (entry.front() == '/' || entry.front() == '\\' || entry.find(':') != std::string_view::npos) { return false; } @@ -58,12 +57,9 @@ struct ArchiveDeleter { using ArchivePtr = std::unique_ptr; -enum class EntryKind { Regular, - Directory }; +enum class EntryKind { Regular, Directory }; -uint16_t ReadU16(const uint8_t* data) { - return static_cast(data[0] | (data[1] << 8)); -} +uint16_t ReadU16(const uint8_t* data) { return static_cast(data[0] | (data[1] << 8)); } uint32_t ReadU32(const uint8_t* data) { return static_cast(data[0]) | (static_cast(data[1]) << 8) | @@ -117,8 +113,8 @@ bool ValidateZipStructure(const std::filesystem::path& zip_path, ILogger& logger const uint64_t absolute_eocd_offset = file_size - tail_size + eocd_offset; if (disk != 0 || central_disk != 0 || entries_on_disk != total_entries || total_entries == 0xffff || - central_size == 0xffffffff || central_offset == 0xffffffff || - central_offset > absolute_eocd_offset || central_size != absolute_eocd_offset - central_offset) { + central_size == 0xffffffff || central_offset == 0xffffffff || central_offset > absolute_eocd_offset || + central_size != absolute_eocd_offset - central_offset) { logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory bounds"); return false; } @@ -168,15 +164,64 @@ ArchivePtr OpenArchive(const std::filesystem::path& zip_path, ILogger& logger) { const auto open_result = archive_read_open_filename(reader.get(), zip_path.c_str(), 64 * 1024); #endif if (open_result != ARCHIVE_OK) { - logger.Log(LogLevel::Warning, - fmt::format("ExtractZip: failed to open '{}': {}", zip_path.string(), - archive_error_string(reader.get()))); + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: failed to open '{}': {}", zip_path.string(), + archive_error_string(reader.get()))); return nullptr; } return reader; } +bool EnsureActualDirectory(const std::filesystem::path& path) { + if (path.empty()) { + return true; + } + + const auto parent = path.parent_path(); + if (parent != path && !EnsureActualDirectory(parent)) { + return false; + } + + std::error_code ec; + auto status = std::filesystem::symlink_status(path, ec); + if (ec && status.type() != std::filesystem::file_type::not_found) { + return false; + } + + if (std::filesystem::is_directory(status)) { + return true; + } + + if (status.type() != std::filesystem::file_type::not_found) { + return false; + } + + ec.clear(); + std::filesystem::create_directory(path, ec); + if (ec) { + return false; + } + + status = std::filesystem::symlink_status(path, ec); + return !ec && std::filesystem::is_directory(status); +} + +bool PrepareDestination(const std::filesystem::path& destination, ILogger& logger) { + if (!EnsureActualDirectory(destination)) { + logger.Log(LogLevel::Warning, + fmt::format("ExtractZip: destination is not a safe directory: '{}'", destination.string())); + return false; + } + + std::error_code ec; + if (!std::filesystem::is_empty(destination, ec) || ec) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: destination is not empty: '{}'", destination.string())); + return false; + } + + return true; +} + bool ValidateArchive(const std::filesystem::path& zip_path, const ZipExtractLimits& limits, ILogger& logger) { auto reader = OpenArchive(zip_path, logger); if (!reader) { @@ -271,7 +316,6 @@ bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem return false; } - std::filesystem::create_directories(destination); uint64_t total_written = 0; archive_entry* entry = nullptr; char buffer[64 * 1024]; @@ -288,11 +332,27 @@ bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem const auto output_path = destination / name; if (archive_entry_filetype(entry) == AE_IFDIR) { - std::filesystem::create_directories(output_path); + if (!EnsureActualDirectory(output_path)) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: unsafe directory path '{}'", output_path.string())); + return false; + } + continue; } - std::filesystem::create_directories(output_path.parent_path()); + if (!EnsureActualDirectory(output_path.parent_path())) { + logger.Log(LogLevel::Warning, + fmt::format("ExtractZip: unsafe directory path '{}'", output_path.parent_path().string())); + return false; + } + + std::error_code ec; + const auto output_status = std::filesystem::symlink_status(output_path, ec); + if (output_status.type() != std::filesystem::file_type::not_found) { + logger.Log(LogLevel::Warning, fmt::format("ExtractZip: refusing pre-existing output '{}'", output_path.string())); + return false; + } + std::ofstream output(output_path, std::ios::binary | std::ios::trunc); if (!output) { logger.Log(LogLevel::Warning, fmt::format("ExtractZip: failed to create '{}'", output_path.string())); @@ -313,8 +373,7 @@ bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem entry_written += static_cast(count); total_written += static_cast(count); - if (entry_written > limits.max_entry_uncompressed_bytes || - total_written > limits.max_total_uncompressed_bytes) { + if (entry_written > limits.max_entry_uncompressed_bytes || total_written > limits.max_total_uncompressed_bytes) { logger.Log(LogLevel::Warning, fmt::format("ExtractZip: extracted size limit exceeded by '{}'", name)); return false; } @@ -332,7 +391,6 @@ bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem return false; } - std::error_code ec; std::filesystem::permissions(output_path, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read, @@ -350,9 +408,7 @@ bool ExtractArchive(const std::filesystem::path& zip_path, const std::filesystem } // namespace -bool ExtractZip(const std::filesystem::path& zip_path, - const std::filesystem::path& destination, - ILogger& logger, +bool ExtractZip(const std::filesystem::path& zip_path, const std::filesystem::path& destination, ILogger& logger, const ZipExtractLimits& limits) { std::error_code ec; if (!std::filesystem::is_regular_file(zip_path, ec)) { @@ -361,7 +417,7 @@ bool ExtractZip(const std::filesystem::path& zip_path, } return ValidateZipStructure(zip_path, logger) && ValidateArchive(zip_path, limits, logger) && - ExtractArchive(zip_path, destination, limits, logger); + PrepareDestination(destination, logger) && ExtractArchive(zip_path, destination, limits, logger); } } // namespace fl diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index a33e6d592..9ef2d2614 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -75,6 +75,7 @@ target_link_libraries(foundry_local_tests foundry_local_static GTest::gtest_main httplib::httplib + ZLIB::ZLIB ) # On Linux, GenAI does dlopen("libonnxruntime.so") internally. Modern GCC emits diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index bc295e881..05e1b46dd 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -2,10 +2,65 @@ // Licensed under the MIT License. #include "ep_detection/cuda_ep_bootstrapper.h" +#include "ep_detection/cuda_ep_manifest.h" +#include "ep_detection/ep_utils.h" +#include "logger.h" +#include "utils/scoped_environment_variable.h" +#include "utils/temp_path.h" + #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace fl { +namespace { + +constexpr uint64_t kMiB = 1024ULL * 1024; +constexpr std::string_view kCdnBase = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/"; +constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_CUDA_EP_LIBRARY"; + +struct ExpectedFile { + std::string_view path; + std::string_view sha256; +}; + +void ExpectArtifact(const EpBundleArtifact& artifact, std::string_view id, std::string_view filename, + std::string_view archive_sha256, uint64_t max_bytes, + const std::vector& expected_files) { + EXPECT_EQ(artifact.id, id); + EXPECT_EQ(artifact.url, std::string(kCdnBase) + std::string(filename)); + EXPECT_TRUE(artifact.is_archive); + EXPECT_EQ(artifact.archive_sha256, archive_sha256); + EXPECT_EQ(artifact.archive_max_bytes, max_bytes); + EXPECT_EQ(artifact.ignored_archive_paths, std::vector{"version.json"}); + ASSERT_EQ(artifact.extracted_files.size(), expected_files.size()); + + for (size_t i = 0; i < expected_files.size(); ++i) { + EXPECT_EQ(artifact.extracted_files[i].relative_path, expected_files[i].path); + EXPECT_EQ(artifact.extracted_files[i].sha256, expected_files[i].sha256); + } +} + +void ExpectUniqueInstalledPaths(const EpBundleManifest& manifest) { + std::unordered_set paths; + for (const auto& artifact : manifest.artifacts) { + for (const auto& file : artifact.extracted_files) { + EXPECT_TRUE(paths.insert(file.relative_path).second) << file.relative_path; + } + } +} + +} // namespace + TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { #if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) @@ -15,4 +70,136 @@ TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { #endif } +TEST(CudaEpManifestTest, WindowsX64MetadataMatches20260805Bundle) { + const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsX64); + ASSERT_TRUE(manifest.has_value()); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260805-050438"); + EXPECT_EQ(manifest->provider_relative_path, "onnxruntime_providers_cuda.dll"); + ASSERT_EQ(manifest->artifacts.size(), 3u); + + ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-x64-20260805-050438.zip", + "b47716cbd9a1c92722a6bc914ca57b0e8efea15f7b1a46eecfb2637cadc1bee5", 640 * kMiB, + { + {"cublas64_12.dll", "9513540e4ec4c51ee9e7304138c2cc255c29a8c181f9e80c38efa25738becd99"}, + {"cublasLt64_12.dll", "b199d1ff892a81b7fd3d57ba1781549609b41500b36008fef326038393ad46c7"}, + {"cudart64_12.dll", "c2c9a9c22a9bcba90e261825968836787b331038047a26770cffb7a583c28344"}, + }); + ExpectArtifact( + manifest->artifacts[1], "cudnn", "cudnn-bins-win-x64-20260805-050438.zip", + "1b065e115c2ac35040053ebe594a8c089906f8cbe5b8d8ed832ba5eb27cdeb5e", 704 * kMiB, + { + {"cudnn64_9.dll", "0d1d71325eb5e91570ab8ba8e399e07bf717ffd76511b2407229a8f45e0b1305"}, + {"cudnn_adv64_9.dll", "6d66bce22502c2582a9c0e5398ee8cc38addce2c837eb6db8786abc650e48dd8"}, + {"cudnn_engines_precompiled64_9.dll", "b410c3b42921afc6e668ff994fce1bf12c5a8a9b1a9445ebee61958bf49b1e0a"}, + {"cudnn_engines_runtime_compiled64_9.dll", + "8e62214495c96b93c6333c084fec49b43f272b7e1977a12fe62275e9070647eb"}, + {"cudnn_graph64_9.dll", "82f710b01d15d20c311009721c771b76360a4954ebf7b5f4a407b0f96587f568"}, + {"cudnn_heuristic64_9.dll", "50719eefb6692074096bf83c87e9cd186f7ce5b953201da33669c1277a61949b"}, + {"cudnn_ops64_9.dll", "49487537744256a3d4365c4792b03bf31130ad1faea0a13eafa219620941d837"}, + }); + ExpectArtifact( + manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-x64-20260805-050438.zip", + "65044a715a2d4b74e77f019988f77c936f5b62973c27cf2d59704cf39057e567", 256 * kMiB, + { + {"onnxruntime-genai-cuda.dll", "612ad6cf3d099431af886537080223a62522e58caba0c7d278b9b4b1eb03c4ce"}, + {"onnxruntime_providers_cuda.dll", "971c1002ce7c16338273f693316bec4862ac74c7efa2ffbd644630cfe10d6e37"}, + }); + ExpectUniqueInstalledPaths(*manifest); +} + +TEST(CudaEpManifestTest, WindowsArm64MetadataMatches20260805Bundle) { + const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsArm64); + ASSERT_TRUE(manifest.has_value()); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260805-050639"); + EXPECT_EQ(manifest->provider_relative_path, "onnxruntime_providers_cuda.dll"); + ASSERT_EQ(manifest->artifacts.size(), 3u); + + ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-arm64-20260805-050639.zip", + "b4e0ce6beea87843d02c7d41e04fee3d1a9fb22e0f4fb5e587914cf4f4b94113", 192 * kMiB, + { + {"cublas64_13.dll", "80b322ce3fe77d1c6c0348e30a31c5f2682da4197680177a179af69275b57997"}, + {"cublasLt64_13.dll", "d13048a5f17deeb1a051189c0d5ac898cdf398c6dfca62d100c6eb39329a1d80"}, + {"cudart64_13.dll", "32504bd5f424a4e73d3bb5ecc69f018538ae371efa0210bd33e88c7c78b9dca7"}, + }); + ExpectArtifact( + manifest->artifacts[1], "cudnn", "cudnn-bins-win-arm64-20260805-050639.zip", + "24347fc6b596ae28c32659c82da688bc386da36228e65329df031c028d8527ad", 192 * kMiB, + { + {"cudnn64_9.dll", "247cecbb33132c829c6ed328b7dd34d077a27d0f0fb0ee0b56469ec6bdfd1c17"}, + {"cudnn_adv64_9.dll", "b624590960a3ce3ac7c3a5fc683912dbd9ba9de20fa1af52db4485c435c78375"}, + {"cudnn_engines_precompiled64_9.dll", "c3be7f8a9091865b7fc94ddd69e62024338d42a188633729e4520244b072da2d"}, + {"cudnn_engines_runtime_compiled64_9.dll", + "bd558d60e1dbeeeee8f59dfec8bd5ce992876f923e2f19f7ef03a4a7e110a89a"}, + {"cudnn_graph64_9.dll", "8f568df300b0733abe2cb35ea6bfcc40d2330db005ab9ebba96d008f6bc0b568"}, + {"cudnn_heuristic64_9.dll", "59d5aad876ab55d30194f36d3f2c5ff90eeaeed502b256c83ed3d6082030f58d"}, + {"cudnn_ops64_9.dll", "c9e0ec0e0a4e659393e15897ed1f6e5bac677e0c0fe7e12290f0386f19477b6b"}, + }); + ExpectArtifact( + manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-arm64-20260805-050639.zip", + "8152d03a0fbef39bd11f5b07dbc6776abd125dbee1dc1d2877a04dc62bbde641", 96 * kMiB, + { + {"onnxruntime-genai-cuda.dll", "5284fdec9d4e9e25d6b4cf129205f0c88d3c2f5e678907b2bc1581b575266016"}, + {"onnxruntime_providers_cuda.dll", "b60cd5a26bc180229c9da0dc635d6b3404c306246708291dfae7c9f72ad5e862"}, + }); + ExpectUniqueInstalledPaths(*manifest); +} + +TEST(CudaEpManifestTest, LinuxX64MetadataMatches20260805Bundle) { + const auto manifest = BuildCudaEpManifest(CudaEpPlatform::LinuxX64); + ASSERT_TRUE(manifest.has_value()); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260805-050706"); + EXPECT_EQ(manifest->provider_relative_path, "libonnxruntime_providers_cuda.so"); + ASSERT_EQ(manifest->artifacts.size(), 1u); + + ExpectArtifact( + manifest->artifacts[0], "cuda-ep", "cuda-ep-linux-x64-20260805-050706.zip", + "2bc3e5949b75d7521d903c958716c06602ddaa5c2a1f98bd12811294db738c37", 448 * kMiB, + { + {"libonnxruntime-genai-cuda.so", "8b26db7a085de61653ebaaa8fc221b720879fe74583eb01204f11bf22638c345"}, + {"libonnxruntime_providers_cuda.so", "da94d951b89dc84c44b10f7faf52b17e675b3f1a13d8f32808264d425d0464bd"}, + }); + ExpectUniqueInstalledPaths(*manifest); +} + +TEST(CudaEpManifestTest, UnsupportedPlatformsHaveNoManifest) { + EXPECT_FALSE(BuildCudaEpManifest(CudaEpPlatform::LinuxArm64).has_value()); + EXPECT_FALSE(BuildCudaEpManifest(CudaEpPlatform::Unsupported).has_value()); +} + +TEST(CudaEpManifestTest, WindowsPreloadIncludesRuntimeDependenciesButExcludesProviderAndCoreOrt) { + const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsX64); + ASSERT_TRUE(manifest.has_value()); + + const auto dependencies = SelectEpBundleDependenciesToPreload("bin", *manifest); + std::vector filenames; + std::transform(dependencies.begin(), dependencies.end(), std::back_inserter(filenames), + [](const auto& path) { return path.filename().string(); }); + + EXPECT_NE(std::find(filenames.begin(), filenames.end(), "cudart64_12.dll"), filenames.end()); + EXPECT_NE(std::find(filenames.begin(), filenames.end(), "cudnn64_9.dll"), filenames.end()); + EXPECT_NE(std::find(filenames.begin(), filenames.end(), "onnxruntime-genai-cuda.dll"), filenames.end()); + EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime_providers_cuda.dll"), filenames.end()); + EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime.dll"), filenames.end()); + EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime-genai.dll"), filenames.end()); +} + +TEST(CudaEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + const auto provider_path = root.path() / "custom_cuda_provider"; + std::ofstream(provider_path, std::ios::binary) << "test provider"; + test::ScopedEnvironmentVariable override(kOverrideEnv, provider_path.string()); + + int registration_count = 0; + CudaEpBootstrapper bootstrapper(root.string(), [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return true; + }); + StderrLogger logger; + + EXPECT_FALSE(bootstrapper.DownloadAndRegister( + false, [](const std::string&, float percent) { return percent != 90.0f; }, logger)); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(registration_count, 0); +} + } // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc index 2f47c80e5..5d9a54632 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -29,9 +29,7 @@ class NullLogger : public ILogger { void Log(LogLevel /*level*/, std::string_view /*message*/) override {} }; -std::vector AsBytes(const std::string& text) { - return std::vector(text.begin(), text.end()); -} +std::vector AsBytes(const std::string& text) { return std::vector(text.begin(), text.end()); } std::string HashOf(const std::vector& bytes) { auto tmp = test::TempPath::CreateTempFile("fl_bundle_installer_hash_"); @@ -95,8 +93,7 @@ class FakeDownloads { std::map call_counts_; }; -EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url, - const std::string& sha256) { +EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url, const std::string& sha256) { EpBundleManifest manifest; manifest.bundle_id = bundle_id; manifest.provider_relative_path = "provider.so"; @@ -105,6 +102,7 @@ EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string .is_archive = false, .archive_sha256 = "", .extracted_files = {}, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "provider.so", .raw_sha256 = sha256, @@ -113,8 +111,8 @@ EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string } EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::string& url, - const std::string& archive_sha256, - std::vector extracted_files) { + const std::string& archive_sha256, std::vector extracted_files, + std::vector ignored_archive_paths = {}) { EpBundleManifest manifest; manifest.bundle_id = bundle_id; manifest.provider_relative_path = "provider.dll"; @@ -123,6 +121,7 @@ EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::st .is_archive = true, .archive_sha256 = archive_sha256, .extracted_files = std::move(extracted_files), + .ignored_archive_paths = std::move(ignored_archive_paths), .archive_max_bytes = 1024 * 1024, .raw_relative_path = "", .raw_sha256 = "", @@ -130,8 +129,8 @@ EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::st return manifest; } -std::optional InstallAndCommit(EpBundleInstaller& installer, - const EpBundleManifest& manifest, ILogger& logger) { +std::optional InstallAndCommit(EpBundleInstaller& installer, const EpBundleManifest& manifest, + ILogger& logger) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); if (!txn) { return std::nullopt; @@ -218,6 +217,7 @@ TEST(EpBundleInstallerTest, ForceDownloadRedownloadsEveryArtifactFromValidActive .is_archive = false, .archive_sha256 = "", .extracted_files = {}, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "first.bin", .raw_sha256 = HashOf(first), @@ -227,6 +227,7 @@ TEST(EpBundleInstallerTest, ForceDownloadRedownloadsEveryArtifactFromValidActive .is_archive = false, .archive_sha256 = "", .extracted_files = {}, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "second.bin", .raw_sha256 = HashOf(second), @@ -237,8 +238,8 @@ TEST(EpBundleInstallerTest, ForceDownloadRedownloadsEveryArtifactFromValidActive NullLogger logger; ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); - auto replacement = installer.EnsureInstalled( - manifest, /*progress_cb=*/nullptr, logger, EpBundleInstallPolicy::ForceDownload); + auto replacement = + installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger, EpBundleInstallPolicy::ForceDownload); ASSERT_NE(replacement, nullptr); EXPECT_EQ(ReadFile(replacement->bin_dir() / "first.bin"), "first"); @@ -274,9 +275,9 @@ TEST(EpBundleInstallerTest, ArchiveHashMismatchRetriesOnceThenSucceeds) { FakeDownloads downloads; downloads.SetSequence("https://example.test/archive.zip", {bad_archive, good_archive}); - auto manifest = MakeArchiveManifest( - "bundle-1", "https://example.test/archive.zip", HashOf(good_archive), - {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); + auto manifest = + MakeArchiveManifest("bundle-1", "https://example.test/archive.zip", HashOf(good_archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; @@ -294,9 +295,8 @@ TEST(EpBundleInstallerTest, ArchiveHashMismatchTwiceFails) { downloads.SetSequence("https://example.test/archive.zip", {bad_archive}); auto manifest = - MakeArchiveManifest( - "bundle-1", "https://example.test/archive.zip", HashOf(AsBytes("expected-archive")), - {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}); + MakeArchiveManifest("bundle-1", "https://example.test/archive.zip", HashOf(AsBytes("expected-archive")), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}); EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; @@ -338,10 +338,10 @@ TEST(EpBundleInstallerTest, MissingExpectedExtractedFileFails) { FakeDownloads downloads; downloads.SetSequence("https://example.test/archive.zip", {archive}); - auto manifest = MakeArchiveManifest( - "bundle-1", "https://example.test/archive.zip", HashOf(archive), - {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("actual-content"))}, - EpBundleFile{.relative_path = "missing.dll", .sha256 = HashOf(AsBytes("whatever"))}}); + auto manifest = + MakeArchiveManifest("bundle-1", "https://example.test/archive.zip", HashOf(archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("actual-content"))}, + EpBundleFile{.relative_path = "missing.dll", .sha256 = HashOf(AsBytes("whatever"))}}); EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; @@ -510,6 +510,67 @@ TEST(EpBundleInstallerTest, StaleStagingDirectoryIsCleanedUpOnNextInstall) { EXPECT_FALSE(std::filesystem::exists(root.path() / "staging" / "leftover-from-a-crash")); } +TEST(EpBundleInstallerTest, RejectsSymlinkedStagingDirectoryWithoutTraversingIt) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto outside = test::TempPath::CreateTempDir("fl_bundle_installer_outside_"); + std::ofstream(outside.path() / "must-remain.txt") << "preserved"; + std::error_code ec; + std::filesystem::create_directory_symlink(outside.path(), root.path() / "staging", ec); + if (ec) { + GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); + } + + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + EXPECT_EQ(installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger), nullptr); + EXPECT_EQ(ReadFile(outside.path() / "must-remain.txt"), "preserved"); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 0); +} + +TEST(EpBundleInstallerTest, RejectsSymlinkedBundlesDirectoryWithoutTraversingIt) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto outside = test::TempPath::CreateTempDir("fl_bundle_installer_outside_"); + std::filesystem::create_directories(outside.path() / "orphan" / "bin"); + std::ofstream(outside.path() / "orphan" / "bin" / "must-remain.txt") << "preserved"; + std::error_code ec; + std::filesystem::create_directory_symlink(outside.path(), root.path() / "bundles", ec); + if (ec) { + GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); + } + + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + EXPECT_EQ(installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger), nullptr); + EXPECT_EQ(ReadFile(outside.path() / "orphan" / "bin" / "must-remain.txt"), "preserved"); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 0); +} + +TEST(EpBundleInstallerTest, RejectsManagedDirectoryThatIsARegularFile) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + std::ofstream(root.path() / "bundles") << "not a directory"; + + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + EXPECT_EQ(installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger), nullptr); + EXPECT_EQ(ReadFile(root.path() / "bundles"), "not a directory"); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 0); +} + TEST(EpBundleInstallerTest, DoesNotCopyUnexpectedFilesFromExistingBundle) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload = AsBytes("content"); @@ -543,9 +604,9 @@ TEST(EpBundleInstallerTest, FreshInstallRejectsArchiveWithUndeclaredExtraFile) { FakeDownloads downloads; downloads.SetSequence("https://example.test/archive.zip", {archive}); - auto manifest = MakeArchiveManifest( - "bundle-1", "https://example.test/archive.zip", HashOf(archive), - {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); + auto manifest = + MakeArchiveManifest("bundle-1", "https://example.test/archive.zip", HashOf(archive), + {EpBundleFile{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("dll-bytes"))}}); EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; @@ -560,6 +621,124 @@ TEST(EpBundleInstallerTest, FreshInstallRejectsArchiveWithUndeclaredExtraFile) { } } +TEST(EpBundleInstallerTest, MultipleArchivesMayIgnoreSamePathWithoutInstallingIt) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + + test::ZipBuilder first_builder; + first_builder.AddEntry("first.dll", AsBytes("first")); + first_builder.AddEntry("version.json", AsBytes("first metadata")); + const auto first_archive = first_builder.Build(); + + test::ZipBuilder second_builder; + second_builder.AddEntry("provider.dll", AsBytes("provider")); + second_builder.AddEntry("version.json", AsBytes("different metadata")); + const auto second_archive = second_builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/first.zip", {first_archive}); + downloads.SetSequence("https://example.test/second.zip", {second_archive}); + + EpBundleManifest manifest; + manifest.bundle_id = "bundle"; + manifest.provider_relative_path = "provider.dll"; + manifest.artifacts = { + EpBundleArtifact{ + .id = "first", + .url = "https://example.test/first.zip", + .is_archive = true, + .archive_sha256 = HashOf(first_archive), + .extracted_files = {{.relative_path = "first.dll", .sha256 = HashOf(AsBytes("first"))}}, + .ignored_archive_paths = {"version.json"}, + .archive_max_bytes = 1024 * 1024, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }, + EpBundleArtifact{ + .id = "second", + .url = "https://example.test/second.zip", + .is_archive = true, + .archive_sha256 = HashOf(second_archive), + .extracted_files = {{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}, + .ignored_archive_paths = {"version.json"}, + .archive_max_bytes = 1024 * 1024, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }, + }; + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + + ASSERT_NE(txn, nullptr); + EXPECT_EQ(ReadFile(txn->bin_dir() / "first.dll"), "first"); + EXPECT_EQ(ReadFile(txn->bin_dir() / "provider.dll"), "provider"); + EXPECT_FALSE(std::filesystem::exists(txn->bin_dir() / "version.json")); + EXPECT_FALSE(std::filesystem::exists(txn->bin_dir().parent_path() / "artifacts")); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); +} + +TEST(EpBundleInstallerTest, RejectsRuntimeAndIgnoredPathOverlapWithoutDownloading) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + FakeDownloads downloads; + const auto payload = AsBytes("provider"); + const auto manifest = + MakeArchiveManifest("bundle", "https://example.test/archive.zip", HashOf(payload), + {{.relative_path = "provider.dll", .sha256 = HashOf(payload)}}, {"provider.dll"}); + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + EXPECT_EQ(installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger), nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/archive.zip"), 0); +} + +TEST(EpBundleInstallerTest, RejectsArchiveMissingDeclaredIgnoredEntryAndCleansPrivateStaging) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("provider")); + const auto archive = builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {archive}); + const auto manifest = + MakeArchiveManifest("bundle", "https://example.test/archive.zip", HashOf(archive), + {{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}, {"version.json"}); + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + EXPECT_EQ(installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger), nullptr); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); +} + +TEST(EpBundleInstallerTest, ReuseDependsOnlyOnInstalledRuntimeFiles) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + test::ZipBuilder builder; + builder.AddEntry("provider.dll", AsBytes("provider")); + builder.AddEntry("version.json", AsBytes("metadata")); + const auto archive = builder.Build(); + + FakeDownloads downloads; + downloads.SetSequence("https://example.test/archive.zip", {archive}); + auto manifest = + MakeArchiveManifest("bundle", "https://example.test/archive.zip", HashOf(archive), + {{.relative_path = "provider.dll", .sha256 = HashOf(AsBytes("provider"))}}, {"version.json"}); + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + + manifest.artifacts.front().ignored_archive_paths = {"different-packaging-metadata.json"}; + auto reused = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + + ASSERT_NE(reused, nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/archive.zip"), 1); + EXPECT_EQ(ReadFile(reused->bin_dir() / "provider.dll"), "provider"); +} + TEST(EpBundleInstallerTest, ReusesValidArtifactsAndDownloadsOnlyMismatches) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); const auto first = AsBytes("first"); @@ -578,6 +757,7 @@ TEST(EpBundleInstallerTest, ReusesValidArtifactsAndDownloadsOnlyMismatches) { .is_archive = false, .archive_sha256 = "", .extracted_files = {}, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "first.bin", .raw_sha256 = HashOf(first), @@ -587,6 +767,7 @@ TEST(EpBundleInstallerTest, ReusesValidArtifactsAndDownloadsOnlyMismatches) { .is_archive = false, .archive_sha256 = "", .extracted_files = {}, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "second.bin", .raw_sha256 = HashOf(second), @@ -623,4 +804,106 @@ TEST(EpBundleInstallerTest, CancellationDuringDownloadFails) { EXPECT_EQ(txn, nullptr); } +TEST(EpBundleInstallerTest, CancellationAtTerminalDownloaderProgressRemovesPartialGeneration) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto cancel_at_artifact_completion = [](const std::string&, float percent) { return percent < 80.0f; }; + + EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_at_artifact_completion, logger), nullptr); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "bundles")); + EXPECT_FALSE(std::filesystem::exists(root.path() / "active")); +} + +TEST(EpBundleInstallerTest, CancellationBeforeReturningNewTransactionRemovesPublishedGeneration) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + auto cancel_before_transaction = [](const std::string&, float percent) { return percent < 90.0f; }; + + EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_before_transaction, logger), nullptr); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "bundles")); + EXPECT_FALSE(std::filesystem::exists(root.path() / "active")); +} + +TEST(EpBundleInstallerTest, CancellationOnVerifiedBundleReuseReturnsNoTransaction) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto payload = AsBytes("content"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider.so", {payload}); + const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + const auto active_generation = ReadFile(root.path() / "active"); + + auto cancel_reuse = [](const std::string&, float percent) { return percent != 90.0f; }; + + EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_reuse, logger), nullptr); + EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1); + EXPECT_EQ(ReadFile(root.path() / "active"), active_generation); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_generation)); +} + +TEST(EpBundleInstallerTest, CancellationAfterCopiedArtifactRemovesStagingCopy) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + const auto first = AsBytes("first"); + const auto second = AsBytes("second"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/first.bin", {first}); + downloads.SetSequence("https://example.test/second.bin", {second}); + + EpBundleManifest manifest; + manifest.bundle_id = "bundle"; + manifest.provider_relative_path = "first.bin"; + manifest.artifacts = { + EpBundleArtifact{.id = "first", + .url = "https://example.test/first.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .ignored_archive_paths = {}, + .archive_max_bytes = 0, + .raw_relative_path = "first.bin", + .raw_sha256 = HashOf(first), + .raw_max_bytes = 1024}, + EpBundleArtifact{.id = "second", + .url = "https://example.test/second.bin", + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .ignored_archive_paths = {}, + .archive_max_bytes = 0, + .raw_relative_path = "second.bin", + .raw_sha256 = HashOf(second), + .raw_max_bytes = 1024}, + }; + + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + const auto active_generation = ReadFile(root.path() / "active"); + const auto active_bin = root.path() / "bundles" / active_generation / "bin"; + std::ofstream(active_bin / "second.bin", std::ios::binary | std::ios::trunc) << "corrupt"; + + auto cancel_after_copy = [](const std::string&, float percent) { return percent < 40.0f; }; + + EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_after_copy, logger), nullptr); + EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); + EXPECT_EQ(downloads.CallCount("https://example.test/first.bin"), 1); + EXPECT_EQ(downloads.CallCount("https://example.test/second.bin"), 1); +} + } // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc index 03467525d..4a120f2de 100644 --- a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc @@ -37,8 +37,8 @@ EpBundleManifest MakeCudaLikeManifest() { {.relative_path = "cudart64_12.dll", .sha256 = "b"}, {.relative_path = "ONNXRUNTIME.DLL", .sha256 = "c"}, {.relative_path = "onnxruntime-genai.dll", .sha256 = "d"}, - {.relative_path = "version.json", .sha256 = "e"}, }, + .ignored_archive_paths = {"version.json"}, .archive_max_bytes = 0, .raw_relative_path = "", .raw_sha256 = "", @@ -53,6 +53,7 @@ EpBundleManifest MakeCudaLikeManifest() { { {.relative_path = "cublas64_12.DLL", .sha256 = "f"}, }, + .ignored_archive_paths = {}, .archive_max_bytes = 0, .raw_relative_path = "", .raw_sha256 = "", @@ -70,8 +71,8 @@ TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadExcludesProviderAndCoreRunt const auto dependencies = SelectEpBundleDependenciesToPreload(bin_dir, manifest); - // Only the two non-provider, non-core-runtime DLLs should remain, in manifest order, with - // version.json (not a DLL) and the provider/core runtime DLLs (matched case-insensitively) excluded. + // Only the two non-provider, non-core-runtime DLLs should remain, in manifest order. Ignored metadata is + // not considered, and provider/core runtime DLLs are excluded case-insensitively. ASSERT_EQ(dependencies.size(), 2u); EXPECT_EQ(dependencies[0], std::filesystem::absolute(bin_dir / "cudart64_12.dll")); EXPECT_EQ(dependencies[1], std::filesystem::absolute(bin_dir / "cublas64_12.DLL")); @@ -93,8 +94,8 @@ TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadReturnsEmptyWhenOnlyProvide { {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = "a"}, {.relative_path = "onnxruntime.dll", .sha256 = "b"}, - {.relative_path = "version.json", .sha256 = "c"}, }, + .ignored_archive_paths = {"version.json"}, .archive_max_bytes = 0, .raw_relative_path = "", .raw_sha256 = "", @@ -113,14 +114,14 @@ TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadHandlesManifestWithNoArtifa EXPECT_TRUE(SelectEpBundleDependenciesToPreload("bin", manifest).empty()); } -TEST(EpUtilsTest, LoadEpBundleDependenciesIsNoOpTrueOnNonWindows) { +TEST(EpUtilsTest, DependencyOwnerLoadIsNoOpOnNonWindows) { #ifndef _WIN32 NullLogger logger; const auto manifest = MakeCudaLikeManifest(); + EpBundleDependencyOwner owner; - // LoadEpBundleDependencies is documented as a no-op that always returns true on non-Windows - // platforms; neither the directory nor the dependency files need to exist here. - EXPECT_TRUE(LoadEpBundleDependencies("nonexistent/bin/dir", manifest, "Test EP", logger)); + EXPECT_TRUE(owner.Load("nonexistent/bin/dir", manifest, "Test EP", logger)); + EXPECT_TRUE(owner.Load("another/nonexistent/bin/dir", manifest, "Test EP", logger)); #else GTEST_SKIP() << "Preloading behavior is exercised on Windows only."; #endif diff --git a/sdk_v2/cpp/test/internal_api/http_download_test.cc b/sdk_v2/cpp/test/internal_api/http_download_test.cc index 43c973697..bf2dbe10a 100644 --- a/sdk_v2/cpp/test/internal_api/http_download_test.cc +++ b/sdk_v2/cpp/test/internal_api/http_download_test.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -39,9 +40,7 @@ constexpr const char* kUserAgent = "FoundryLocal"; /// Captures log output so a failed download surfaces the downloader's own diagnostics. class RecordingLogger : public ILogger { public: - void Log(LogLevel level, std::string_view message) override { - entries.emplace_back(level, std::string(message)); - } + void Log(LogLevel level, std::string_view message) override { entries.emplace_back(level, std::string(message)); } std::string Dump() const { std::ostringstream oss; @@ -56,6 +55,22 @@ class RecordingLogger : public ILogger { } // namespace +TEST(ContentLengthHeaderTest, AcceptsUnsignedDecimalWithSurroundingHttpWhitespace) { + EXPECT_EQ(ParseContentLengthHeader("0"), 0); + EXPECT_EQ(ParseContentLengthHeader("\t 12345 \t"), 12345); + EXPECT_EQ(ParseContentLengthHeader("9223372036854775807"), std::numeric_limits::max()); +} + +TEST(ContentLengthHeaderTest, RejectsEmptyMalformedOverflowAndNegativeValues) { + EXPECT_EQ(ParseContentLengthHeader(""), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader(" \t"), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader("12x"), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader("1 2"), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader("+12"), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader("-1"), std::nullopt); + EXPECT_EQ(ParseContentLengthHeader("9223372036854775808"), std::nullopt); +} + // Downloads the real WebGPU EP zip and validates the success path end-to-end: returns // true, writes a non-empty file, and reports a terminal 100% progress callback. TEST(DISABLED_HttpDownload, DownloadsWebGpuZip) { @@ -66,12 +81,9 @@ TEST(DISABLED_HttpDownload, DownloadsWebGpuZip) { std::atomic cancel{false}; bool ok = HttpDownloadFile( - kWebGpuZipUrl, dest.path(), kUserAgent, &cancel, - [&progress](float pct) { progress.push_back(pct); }, - logger); + kWebGpuZipUrl, dest.path(), kUserAgent, &cancel, [&progress](float pct) { progress.push_back(pct); }, logger); - ASSERT_TRUE(ok) << "WebGPU zip download failed. Logger output:\n" - << logger.Dump(); + ASSERT_TRUE(ok) << "WebGPU zip download failed. Logger output:\n" << logger.Dump(); ASSERT_TRUE(fs::exists(dest.path())); EXPECT_GT(fs::file_size(dest.path()), 0u); @@ -88,8 +100,8 @@ TEST(DISABLED_HttpDownload, ReturnsFalseAndWritesNoFileOnUnresolvableHost) { RecordingLogger logger; auto dest = TempPath::CreateTempFile("fl_webgpu_zip_unresolvable_"); - bool ok = HttpDownloadFile("https://foundry-local-test.invalid/webgpu_ep_0.1.0_win-x64.zip", dest.path(), - kUserAgent, /*cancel_flag=*/nullptr, /*progress_cb=*/{}, logger); + bool ok = HttpDownloadFile("https://foundry-local-test.invalid/webgpu_ep_0.1.0_win-x64.zip", dest.path(), kUserAgent, + /*cancel_flag=*/nullptr, /*progress_cb=*/{}, logger); EXPECT_FALSE(ok); EXPECT_FALSE(fs::exists(dest.path())); diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc index 7048a3f0e..6fec4fddb 100644 --- a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -3,17 +3,14 @@ #include "ep_detection/webgpu_ep_bootstrapper.h" #include "logger.h" +#include "utils/scoped_environment_variable.h" #include "utils/temp_path.h" #include -#include #include #include -#include -#include #include -#include #include #include @@ -24,68 +21,10 @@ namespace { constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; constexpr const char* kScopedEnvironmentVariableTestEnv = "FOUNDRY_LOCAL_SCOPED_ENVIRONMENT_VARIABLE_TEST"; -std::optional GetEnvValue(const char* name) { -#ifdef _WIN32 - char* value = nullptr; - size_t length = 0; - const auto error = _dupenv_s(&value, &length, name); - const std::unique_ptr buffer(value, &std::free); - if (error != 0) { - throw std::system_error(error, std::generic_category(), "_dupenv_s failed"); - } - - if (buffer == nullptr) { - return std::nullopt; - } - - return std::string(buffer.get()); -#else - const auto* value = std::getenv(name); - if (value == nullptr) { - return std::nullopt; - } - - return std::string(value); -#endif -} - -void SetEnvValue(const char* name, const std::optional& value) { -#ifdef _WIN32 - _putenv_s(name, value.value_or("").c_str()); -#else - if (value.has_value()) { - setenv(name, value->c_str(), 1); - } else { - unsetenv(name); - } -#endif -} - -class ScopedEnvironmentVariable { - public: - ScopedEnvironmentVariable(const char* name, std::string value) - : name_(name), - previous_(GetEnvValue(name)) { - SetEnvValue(name_, value); - } - - ~ScopedEnvironmentVariable() { - SetEnvValue(name_, previous_); - } - - ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; - ScopedEnvironmentVariable& operator=(const ScopedEnvironmentVariable&) = delete; - - private: - const char* name_; - std::optional previous_; -}; - } // namespace TEST(WebGpuEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__APPLE__) && defined(__aarch64__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || (defined(__APPLE__) && defined(__aarch64__)) EXPECT_TRUE(WebGpuEpBootstrapper::IsSupportedPlatform()); #else EXPECT_FALSE(WebGpuEpBootstrapper::IsSupportedPlatform()); @@ -93,33 +32,33 @@ TEST(WebGpuEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { } TEST(WebGpuEpBootstrapperTest, ScopedEnvironmentVariableRestoresExistingValue) { - ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); + test::ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); { - ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); - EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "during"); + test::ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); + EXPECT_EQ(test::GetEnvironmentVariable(kScopedEnvironmentVariableTestEnv), "during"); } - EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "before"); + EXPECT_EQ(test::GetEnvironmentVariable(kScopedEnvironmentVariableTestEnv), "before"); } TEST(WebGpuEpBootstrapperTest, ScopedEnvironmentVariableRestoresUnsetValue) { - ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); - SetEnvValue(kScopedEnvironmentVariableTestEnv, std::nullopt); + test::ScopedEnvironmentVariable restore_original(kScopedEnvironmentVariableTestEnv, "before"); + test::SetEnvironmentVariable(kScopedEnvironmentVariableTestEnv, std::nullopt); { - ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); - EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), "during"); + test::ScopedEnvironmentVariable environment(kScopedEnvironmentVariableTestEnv, "during"); + EXPECT_EQ(test::GetEnvironmentVariable(kScopedEnvironmentVariableTestEnv), "during"); } - EXPECT_EQ(GetEnvValue(kScopedEnvironmentVariableTestEnv), std::nullopt); + EXPECT_EQ(test::GetEnvironmentVariable(kScopedEnvironmentVariableTestEnv), std::nullopt); } TEST(WebGpuEpBootstrapperTest, OverrideRegistersUsingExistingProviderConvention) { auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); const auto provider_path = root.path() / "custom_webgpu_provider"; std::ofstream(provider_path, std::ios::binary) << "test provider"; - ScopedEnvironmentVariable override(kOverrideEnv, provider_path.string()); + test::ScopedEnvironmentVariable override(kOverrideEnv, provider_path.string()); std::string registered_name; std::filesystem::path registered_path; @@ -164,4 +103,23 @@ TEST(WebGpuEpBootstrapperTest, OverrideRegistersUsingExistingProviderConvention) EXPECT_EQ(progress[0], std::make_pair(std::string("WebGpuExecutionProvider"), 100.0f)); } +TEST(WebGpuEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) { + auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); + const auto provider_path = root.path() / "custom_webgpu_provider"; + std::ofstream(provider_path, std::ios::binary) << "test provider"; + test::ScopedEnvironmentVariable override(kOverrideEnv, provider_path.string()); + + int registration_count = 0; + WebGpuEpBootstrapper bootstrapper(root.string(), [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return true; + }); + StderrLogger logger; + + EXPECT_FALSE(bootstrapper.DownloadAndRegister( + false, [](const std::string&, float percent) { return percent != 90.0f; }, logger)); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(registration_count, 0); +} + } // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc index 5fd61d2d0..8f72de0f0 100644 --- a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc +++ b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc @@ -19,62 +19,36 @@ namespace fl { -TEST(IsSafeArchiveEntryTest, AcceptsSimpleFilename) { - EXPECT_TRUE(IsSafeArchiveEntry("file.bin")); -} +TEST(IsSafeArchiveEntryTest, AcceptsSimpleFilename) { EXPECT_TRUE(IsSafeArchiveEntry("file.bin")); } -TEST(IsSafeArchiveEntryTest, AcceptsNestedRelativePath) { - EXPECT_TRUE(IsSafeArchiveEntry("dir/sub/file.bin")); -} +TEST(IsSafeArchiveEntryTest, AcceptsNestedRelativePath) { EXPECT_TRUE(IsSafeArchiveEntry("dir/sub/file.bin")); } -TEST(IsSafeArchiveEntryTest, AcceptsBackslashRelativePath) { - EXPECT_TRUE(IsSafeArchiveEntry("dir\\sub\\file.bin")); -} +TEST(IsSafeArchiveEntryTest, AcceptsBackslashRelativePath) { EXPECT_TRUE(IsSafeArchiveEntry("dir\\sub\\file.bin")); } -TEST(IsSafeArchiveEntryTest, AcceptsEmpty) { - EXPECT_TRUE(IsSafeArchiveEntry("")); -} +TEST(IsSafeArchiveEntryTest, AcceptsEmpty) { EXPECT_TRUE(IsSafeArchiveEntry("")); } TEST(IsSafeArchiveEntryTest, AcceptsDotComponent) { // A single "." component is benign and commonly emitted by tar. EXPECT_TRUE(IsSafeArchiveEntry("./file.bin")); } -TEST(IsSafeArchiveEntryTest, RejectsLeadingParent) { - EXPECT_FALSE(IsSafeArchiveEntry("../escape.txt")); -} +TEST(IsSafeArchiveEntryTest, RejectsLeadingParent) { EXPECT_FALSE(IsSafeArchiveEntry("../escape.txt")); } -TEST(IsSafeArchiveEntryTest, RejectsLeadingParentBackslash) { - EXPECT_FALSE(IsSafeArchiveEntry("..\\escape.txt")); -} +TEST(IsSafeArchiveEntryTest, RejectsLeadingParentBackslash) { EXPECT_FALSE(IsSafeArchiveEntry("..\\escape.txt")); } -TEST(IsSafeArchiveEntryTest, RejectsMidPathParent) { - EXPECT_FALSE(IsSafeArchiveEntry("dir/../escape.txt")); -} +TEST(IsSafeArchiveEntryTest, RejectsMidPathParent) { EXPECT_FALSE(IsSafeArchiveEntry("dir/../escape.txt")); } -TEST(IsSafeArchiveEntryTest, RejectsTrailingParent) { - EXPECT_FALSE(IsSafeArchiveEntry("dir/..")); -} +TEST(IsSafeArchiveEntryTest, RejectsTrailingParent) { EXPECT_FALSE(IsSafeArchiveEntry("dir/..")); } -TEST(IsSafeArchiveEntryTest, RejectsDeepParentChain) { - EXPECT_FALSE(IsSafeArchiveEntry("a/b/../../../etc/passwd")); -} +TEST(IsSafeArchiveEntryTest, RejectsDeepParentChain) { EXPECT_FALSE(IsSafeArchiveEntry("a/b/../../../etc/passwd")); } -TEST(IsSafeArchiveEntryTest, RejectsAbsolutePosixPath) { - EXPECT_FALSE(IsSafeArchiveEntry("/etc/passwd")); -} +TEST(IsSafeArchiveEntryTest, RejectsAbsolutePosixPath) { EXPECT_FALSE(IsSafeArchiveEntry("/etc/passwd")); } -TEST(IsSafeArchiveEntryTest, RejectsLeadingBackslash) { - EXPECT_FALSE(IsSafeArchiveEntry("\\Windows\\System32")); -} +TEST(IsSafeArchiveEntryTest, RejectsLeadingBackslash) { EXPECT_FALSE(IsSafeArchiveEntry("\\Windows\\System32")); } -TEST(IsSafeArchiveEntryTest, RejectsWindowsDriveLetter) { - EXPECT_FALSE(IsSafeArchiveEntry("C:\\Windows\\System32")); -} +TEST(IsSafeArchiveEntryTest, RejectsWindowsDriveLetter) { EXPECT_FALSE(IsSafeArchiveEntry("C:\\Windows\\System32")); } -TEST(IsSafeArchiveEntryTest, RejectsLowerCaseDriveLetter) { - EXPECT_FALSE(IsSafeArchiveEntry("c:/Windows")); -} +TEST(IsSafeArchiveEntryTest, RejectsLowerCaseDriveLetter) { EXPECT_FALSE(IsSafeArchiveEntry("c:/Windows")); } TEST(IsSafeArchiveEntryTest, RejectsAnyColon) { // Defensive: archive entries should never legitimately contain ':'. @@ -104,9 +78,7 @@ std::vector ReadFileBytes(const std::filesystem::path& path) { return std::vector((std::istreambuf_iterator(in)), std::istreambuf_iterator()); } -std::vector AsBytes(const std::string& text) { - return std::vector(text.begin(), text.end()); -} +std::vector AsBytes(const std::string& text) { return std::vector(text.begin(), text.end()); } } // namespace @@ -152,6 +124,92 @@ TEST(ExtractZipTest, ExtractsNestedDirectoriesAndMultipleEntries) { std::filesystem::remove(zip_path); } +TEST(ExtractZipTest, RejectsDestinationRootSymlink) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello")); + const auto zip_path = builder.WriteToTempFile(); + auto root = test::TempPath::CreateTempDir("fl_zip_extract_root_"); + auto outside = test::TempPath::CreateTempDir("fl_zip_extract_outside_"); + const auto destination = root.path() / "destination"; + std::error_code ec; + std::filesystem::create_directory_symlink(outside.path(), destination, ec); + if (ec) { + std::filesystem::remove(zip_path); + GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); + } + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, destination, logger)); + EXPECT_TRUE(std::filesystem::is_empty(outside.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsDestinationUnderSymlinkedDirectoryComponent) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello")); + const auto zip_path = builder.WriteToTempFile(); + auto root = test::TempPath::CreateTempDir("fl_zip_extract_root_"); + auto outside = test::TempPath::CreateTempDir("fl_zip_extract_outside_"); + std::filesystem::create_directory(outside.path() / "destination"); + const auto component = root.path() / "component"; + std::error_code ec; + std::filesystem::create_directory_symlink(outside.path(), component, ec); + if (ec) { + std::filesystem::remove(zip_path); + GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); + } + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, component / "destination", logger)); + EXPECT_TRUE(std::filesystem::is_empty(outside.path() / "destination")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsRegularFileDestination) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello")); + const auto zip_path = builder.WriteToTempFile(); + auto destination = test::TempPath::CreateTempFile("fl_zip_extract_dest_"); + std::ofstream(destination.path()) << "not a directory"; + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, destination.path(), logger)); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsNonEmptyDestination) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("hello")); + const auto zip_path = builder.WriteToTempFile(); + auto destination = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + std::ofstream(destination.path() / "existing.txt") << "existing"; + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, destination.path(), logger)); + EXPECT_EQ(ReadFileBytes(destination.path() / "existing.txt"), AsBytes("existing")); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsPreExistingOutputLeafSymlink) { + ZipBuilder builder; + builder.AddEntry("hello.txt", AsBytes("replacement")); + const auto zip_path = builder.WriteToTempFile(); + auto destination = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + auto outside = test::TempPath::CreateTempFile("fl_zip_extract_outside_"); + std::ofstream(outside.path(), std::ios::binary | std::ios::trunc) << "original"; + std::error_code ec; + std::filesystem::create_symlink(outside.path(), destination.path() / "hello.txt", ec); + if (ec) { + std::filesystem::remove(zip_path); + GTEST_SKIP() << "File symlinks are unavailable: " << ec.message(); + } + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, destination.path(), logger)); + EXPECT_EQ(ReadFileBytes(outside.path()), AsBytes("original")); + std::filesystem::remove(zip_path); +} + TEST(ExtractZipTest, RejectsPathTraversalEntry) { ZipBuilder builder; builder.AddEntry("../escape.txt", AsBytes("evil")); @@ -254,8 +312,7 @@ TEST(ExtractZipTest, FailsOnTruncatedArchive) { auto path = test::MakeUniqueTempPath("fl_zip_extract_truncated_"); { std::ofstream out(path, std::ios::binary); - out.write(reinterpret_cast(full_bytes.data()), - static_cast(full_bytes.size() / 2)); + out.write(reinterpret_cast(full_bytes.data()), static_cast(full_bytes.size() / 2)); } auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); NullLogger logger; diff --git a/sdk_v2/cpp/test/utils/scoped_environment_variable.h b/sdk_v2/cpp/test/utils/scoped_environment_variable.h new file mode 100644 index 000000000..efc66b342 --- /dev/null +++ b/sdk_v2/cpp/test/utils/scoped_environment_variable.h @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace fl::test { + +inline std::optional GetEnvironmentVariable(const char* name) { +#ifdef _WIN32 + char* value = nullptr; + size_t length = 0; + const auto error = _dupenv_s(&value, &length, name); + const std::unique_ptr buffer(value, &std::free); + if (error != 0) { + throw std::system_error(error, std::generic_category(), "_dupenv_s failed"); + } + + if (buffer == nullptr) { + return std::nullopt; + } + + return std::string(buffer.get()); +#else + const auto* value = std::getenv(name); + return value == nullptr ? std::nullopt : std::make_optional(value); +#endif +} + +inline void SetEnvironmentVariable(const char* name, const std::optional& value) { +#ifdef _WIN32 + const auto error = _putenv_s(name, value.value_or("").c_str()); + if (error != 0) { + throw std::system_error(error, std::generic_category(), "_putenv_s failed"); + } +#else + const auto error = value.has_value() ? setenv(name, value->c_str(), 1) : unsetenv(name); + if (error != 0) { + throw std::system_error(errno, std::generic_category(), "failed to update environment"); + } +#endif +} + +class ScopedEnvironmentVariable { + public: + ScopedEnvironmentVariable(const char* name, std::string value) + : name_(name), previous_(GetEnvironmentVariable(name)) { + SetEnvironmentVariable(name_, std::move(value)); + } + + ~ScopedEnvironmentVariable() { + try { + SetEnvironmentVariable(name_, previous_); + } catch (...) { + } + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& operator=(const ScopedEnvironmentVariable&) = delete; + + private: + const char* name_; + std::optional previous_; +}; + +} // namespace fl::test diff --git a/sdk_v2/cpp/test/utils/zip_builder.h b/sdk_v2/cpp/test/utils/zip_builder.h index 9046ddf49..d5d4b241d 100644 --- a/sdk_v2/cpp/test/utils/zip_builder.h +++ b/sdk_v2/cpp/test/utils/zip_builder.h @@ -71,7 +71,6 @@ class ZipBuilder { void AddDirectory(const std::string& name) { Entry entry; entry.name = name; - entry.is_directory = true; entries_.push_back(std::move(entry)); } @@ -161,7 +160,6 @@ class ZipBuilder { uint16_t compression_method = 0; uint32_t external_attrs = 0; uint8_t host_os = 0; - bool is_directory = false; }; std::vector entries_; From fa6559d54ad2a2d7730582de1aa1dd868e97ed4d Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Wed, 5 Aug 2026 22:52:50 +0000 Subject: [PATCH 04/11] Address pr review comments --- sdk_v2/cpp/src/http/http_download.cc | 12 +-- sdk_v2/cpp/src/http/http_download.h | 2 + sdk_v2/cpp/src/manager.h | 12 +-- sdk_v2/cpp/src/util/zip_extract.cc | 73 ++++++++++++++++--- .../test/internal_api/http_download_test.cc | 9 +++ .../cpp/test/internal_api/zip_extract_test.cc | 41 +++++++++++ sdk_v2/cpp/test/utils/zip_builder.h | 9 ++- 7 files changed, 137 insertions(+), 21 deletions(-) diff --git a/sdk_v2/cpp/src/http/http_download.cc b/sdk_v2/cpp/src/http/http_download.cc index a1e680193..257a34155 100644 --- a/sdk_v2/cpp/src/http/http_download.cc +++ b/sdk_v2/cpp/src/http/http_download.cc @@ -55,6 +55,10 @@ std::optional ParseContentLengthHeader(std::string_view value) { return parsed; } +bool ContentLengthMatchesBody(int64_t content_length, int64_t bytes_downloaded) { + return content_length < 0 || content_length == bytes_downloaded; +} + bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, std::atomic* cancel_flag, std::function progress_cb, ILogger& logger, int64_t max_bytes) { @@ -194,11 +198,9 @@ bool HttpDownloadFile(const std::string& url, const std::filesystem::path& desti return false; } - // detect truncated transfer. If the server promised a content length and - // we received fewer bytes, surface the error rather than reporting success. - if (content_length > 0 && bytes_downloaded < content_length) { - logger.Log(LogLevel::Warning, MakeString("HTTP download truncated for ", log_url, ": got ", bytes_downloaded, - " of ", content_length, " bytes")); + if (!ContentLengthMatchesBody(content_length, bytes_downloaded)) { + logger.Log(LogLevel::Warning, MakeString("HTTP download length mismatch for ", log_url, ": got ", + bytes_downloaded, " bytes, expected ", content_length)); remove_destination(); return false; } diff --git a/sdk_v2/cpp/src/http/http_download.h b/sdk_v2/cpp/src/http/http_download.h index ea91c3f69..018179568 100644 --- a/sdk_v2/cpp/src/http/http_download.h +++ b/sdk_v2/cpp/src/http/http_download.h @@ -16,6 +16,8 @@ class ILogger; std::optional ParseContentLengthHeader(std::string_view value); +bool ContentLengthMatchesBody(int64_t content_length, int64_t bytes_downloaded); + /// Download a file from an HTTP(S) URL to a local path. /// Supports progress reporting, cancellation, and an optional size cap. /// @param url The URL to download from. diff --git a/sdk_v2/cpp/src/manager.h b/sdk_v2/cpp/src/manager.h index bd2020b5f..a496d865e 100644 --- a/sdk_v2/cpp/src/manager.h +++ b/sdk_v2/cpp/src/manager.h @@ -122,11 +122,13 @@ class Manager { // ep_detector_ — owns EP bootstrappers and dependency handles; // reset after provider unregistration and before OrtEnv release // telemetry_ — used throughout - // catalog_ — owns all Model instances. used by download_manager, model_load_manager, and web - // service download_manager_ — uses ModelInfo owned by catalog model_load_manager_ — holds loaded model - // state referencing catalog models session_manager_ — tracks all active sessions. destroyed after web - // service, before models shutdown_requested_ — atomic flag checked by subsystems and the host process web - // service members — use catalog, model_load_manager, session_manager, telemetry, logger + // catalog_ — owns all Model instances; used by download_manager_, model_load_manager_, + // and the web service + // download_manager_ — uses ModelInfo owned by catalog_ + // model_load_manager_ — holds loaded model state referencing catalog models + // session_manager_ — tracks active sessions; destroyed after the web service and before models + // shutdown_requested_ — atomic flag checked by subsystems and the host process + // web service members — use catalog_, model_load_manager_, session_manager_, telemetry_, and logger_ // Configuration config_; const OrtApi* ort_api_ = nullptr; diff --git a/sdk_v2/cpp/src/util/zip_extract.cc b/sdk_v2/cpp/src/util/zip_extract.cc index 027c22dbd..dd911742b 100644 --- a/sdk_v2/cpp/src/util/zip_extract.cc +++ b/sdk_v2/cpp/src/util/zip_extract.cc @@ -70,6 +70,7 @@ bool ValidateZipStructure(const std::filesystem::path& zip_path, ILogger& logger constexpr uint32_t eocd_signature = 0x06054b50; constexpr uint32_t central_directory_signature = 0x02014b50; constexpr uint64_t eocd_size = 22; + constexpr uint64_t central_header_size = 46; constexpr uint64_t max_comment_size = 65535; std::error_code ec; @@ -119,15 +120,44 @@ bool ValidateZipStructure(const std::filesystem::path& zip_path, ILogger& logger return false; } - if (total_entries > 0) { - uint8_t signature[4]; - input.clear(); - input.seekg(static_cast(central_offset)); - input.read(reinterpret_cast(signature), sizeof(signature)); - if (!input || ReadU32(signature) != central_directory_signature) { + input.clear(); + input.seekg(static_cast(central_offset)); + uint64_t central_bytes_read = 0; + for (uint16_t i = 0; i < total_entries; ++i) { + uint8_t header[central_header_size]; + input.read(reinterpret_cast(header), sizeof(header)); + if (!input || ReadU32(header) != central_directory_signature) { logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory signature"); return false; } + + const auto compression_method = ReadU16(header + 10); + if (compression_method != 0 && compression_method != 8) { + logger.Log(LogLevel::Warning, + fmt::format("ExtractZip: unsupported compression method {}", compression_method)); + return false; + } + + const uint64_t variable_size = + static_cast(ReadU16(header + 28)) + ReadU16(header + 30) + ReadU16(header + 32); + const uint64_t record_size = central_header_size + variable_size; + if (record_size > central_size - central_bytes_read) { + logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory entry bounds"); + return false; + } + + input.seekg(static_cast(variable_size), std::ios::cur); + if (!input) { + logger.Log(LogLevel::Warning, "ExtractZip: invalid central-directory entry"); + return false; + } + + central_bytes_read += record_size; + } + + if (central_bytes_read != central_size) { + logger.Log(LogLevel::Warning, "ExtractZip: central-directory entry count does not match its size"); + return false; } return true; @@ -143,13 +173,37 @@ std::string NormalizeEntryName(std::string_view name) { } std::string ComparisonKey(std::string value) { -#ifdef _WIN32 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); -#endif return value; } +bool IsPortableArchivePath(std::string_view value) { + for (const auto& component_path : std::filesystem::path(value)) { + const auto component = component_path.generic_string(); + if (component.empty() || component.back() == ' ' || component.back() == '.') { + return false; + } + + if (std::any_of(component.begin(), component.end(), [](unsigned char ch) { + return ch < 32 || ch == '<' || ch == '>' || ch == '"' || ch == '|' || ch == '?' || ch == '*'; + })) { + return false; + } + + auto device_name = component.substr(0, component.find('.')); + std::transform(device_name.begin(), device_name.end(), device_name.begin(), + [](unsigned char ch) { return static_cast(std::toupper(ch)); }); + if (device_name == "CON" || device_name == "PRN" || device_name == "AUX" || device_name == "NUL" || + (device_name.size() == 4 && (device_name.starts_with("COM") || device_name.starts_with("LPT")) && + device_name[3] >= '1' && device_name[3] <= '9')) { + return false; + } + } + + return true; +} + ArchivePtr OpenArchive(const std::filesystem::path& zip_path, ILogger& logger) { ArchivePtr reader(archive_read_new()); if (!reader) { @@ -248,7 +302,8 @@ bool ValidateArchive(const std::filesystem::path& zip_path, const ZipExtractLimi } const auto normalized = NormalizeEntryName(name); - if (normalized.empty() || normalized == "." || !IsSafeArchiveEntry(normalized)) { + if (normalized.empty() || normalized == "." || !IsSafeArchiveEntry(normalized) || + !IsPortableArchivePath(normalized)) { logger.Log(LogLevel::Warning, fmt::format("ExtractZip: invalid archive entry '{}'", name)); return false; } diff --git a/sdk_v2/cpp/test/internal_api/http_download_test.cc b/sdk_v2/cpp/test/internal_api/http_download_test.cc index bf2dbe10a..84b5cdeac 100644 --- a/sdk_v2/cpp/test/internal_api/http_download_test.cc +++ b/sdk_v2/cpp/test/internal_api/http_download_test.cc @@ -71,6 +71,15 @@ TEST(ContentLengthHeaderTest, RejectsEmptyMalformedOverflowAndNegativeValues) { EXPECT_EQ(ParseContentLengthHeader("9223372036854775808"), std::nullopt); } +TEST(ContentLengthHeaderTest, RequiresExactBodyLengthWhenHeaderIsPresent) { + EXPECT_TRUE(ContentLengthMatchesBody(-1, 123)); + EXPECT_TRUE(ContentLengthMatchesBody(0, 0)); + EXPECT_TRUE(ContentLengthMatchesBody(123, 123)); + EXPECT_FALSE(ContentLengthMatchesBody(0, 1)); + EXPECT_FALSE(ContentLengthMatchesBody(123, 122)); + EXPECT_FALSE(ContentLengthMatchesBody(123, 124)); +} + // Downloads the real WebGPU EP zip and validates the success path end-to-end: returns // true, writes a non-empty file, and reports a terminal 100% progress callback. TEST(DISABLED_HttpDownload, DownloadsWebGpuZip) { diff --git a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc index 8f72de0f0..1aadfc55d 100644 --- a/sdk_v2/cpp/test/internal_api/zip_extract_test.cc +++ b/sdk_v2/cpp/test/internal_api/zip_extract_test.cc @@ -258,6 +258,47 @@ TEST(ExtractZipTest, RejectsDuplicateEntries) { std::filesystem::remove(zip_path); } +TEST(ExtractZipTest, RejectsUnsupportedCompressionBeforeWritingAnyEntries) { + ZipBuilder builder; + builder.AddEntry("good.txt", AsBytes("good")); + builder.AddEntryWithCompressionMethod("unsupported.txt", AsBytes("unsupported"), 12); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsCaseInsensitivePathAliasesBeforeWriting) { + ZipBuilder builder; + builder.AddEntry("FILE.txt", AsBytes("first")); + builder.AddEntry("file.txt", AsBytes("second")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)); + EXPECT_TRUE(std::filesystem::is_empty(dest.path())); + std::filesystem::remove(zip_path); +} + +TEST(ExtractZipTest, RejectsWin32TrimmedAndReservedPathComponentsBeforeWriting) { + for (const auto* unsafe_name : {"file.", "file ", "CON", "nul.txt", "dir/COM1.bin", "dir/LPT9"}) { + ZipBuilder builder; + builder.AddEntry("good.txt", AsBytes("good")); + builder.AddEntry(unsafe_name, AsBytes("unsafe")); + auto zip_path = builder.WriteToTempFile(); + auto dest = test::TempPath::CreateTempDir("fl_zip_extract_dest_"); + NullLogger logger; + + EXPECT_FALSE(ExtractZip(zip_path, dest.path(), logger)) << unsafe_name; + EXPECT_TRUE(std::filesystem::is_empty(dest.path())) << unsafe_name; + std::filesystem::remove(zip_path); + } +} + TEST(ExtractZipTest, RejectsEntryCountOverLimit) { ZipBuilder builder; builder.AddEntry("a.txt", AsBytes("a")); diff --git a/sdk_v2/cpp/test/utils/zip_builder.h b/sdk_v2/cpp/test/utils/zip_builder.h index d5d4b241d..50df1f23d 100644 --- a/sdk_v2/cpp/test/utils/zip_builder.h +++ b/sdk_v2/cpp/test/utils/zip_builder.h @@ -55,12 +55,17 @@ class ZipBuilder { /// @param unix_mode Packed into external_attrs high word when host_os == 3 (Unix); 0 to omit. void AddEntry(const std::string& name, const std::vector& data, bool compress = false, uint32_t unix_mode = 0) { + AddEntryWithCompressionMethod(name, data, compress ? 8 : 0, unix_mode); + } + + void AddEntryWithCompressionMethod(const std::string& name, const std::vector& data, + uint16_t compression_method, uint32_t unix_mode = 0) { Entry entry; entry.name = name; entry.crc = crc32(0, data.data(), static_cast(data.size())); entry.uncompressed_size = static_cast(data.size()); - entry.compression_method = compress ? 8 : 0; - entry.data = compress ? zip_builder_detail::RawDeflate(data) : data; + entry.compression_method = compression_method; + entry.data = compression_method == 8 ? zip_builder_detail::RawDeflate(data) : data; entry.compressed_size = static_cast(entry.data.size()); entry.host_os = unix_mode != 0 ? 3 : 0; entry.external_attrs = unix_mode != 0 ? (unix_mode << 16) : 0; From c0d25efcd55dcb0c16232c9f3a0c39cf9d61bdad Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Thu, 6 Aug 2026 05:33:02 +0000 Subject: [PATCH 05/11] 0.15.2 and pipeline failures --- .pipelines/foundry-local-packaging.yml | 2 +- .pipelines/v2/sdk_v2-pipeline-plan.md | 2 +- sdk_v2/cpp/test/utils/temp_path.h | 3 ++- sdk_v2/deps_versions.json | 2 +- sdk_v2/js/test/install-native.test.ts | 12 ++++++------ 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.pipelines/foundry-local-packaging.yml b/.pipelines/foundry-local-packaging.yml index 9ca3008cf..99fc1e03c 100644 --- a/.pipelines/foundry-local-packaging.yml +++ b/.pipelines/foundry-local-packaging.yml @@ -58,7 +58,7 @@ variables: - name: cppOrtVersion value: '1.28.0' - name: cppGenaiVersion - value: '0.15.1' + value: '0.15.2' - name: cppWinmlVersion value: '2.1.70' - name: cppBuildConfig diff --git a/.pipelines/v2/sdk_v2-pipeline-plan.md b/.pipelines/v2/sdk_v2-pipeline-plan.md index 521aedd09..b7288179e 100644 --- a/.pipelines/v2/sdk_v2-pipeline-plan.md +++ b/.pipelines/v2/sdk_v2-pipeline-plan.md @@ -245,7 +245,7 @@ purposes: Versions are pipeline-level variables, currently: * `ortVersion` `1.28.0` (`Microsoft.ML.OnnxRuntime`) -* `genaiVersion` `0.15.1` (`Microsoft.ML.OnnxRuntimeGenAI.Foundry`) +* `genaiVersion` `0.15.2` (`Microsoft.ML.OnnxRuntimeGenAI.Foundry`) * `winmlVersion` `2.1.70` (`Microsoft.Windows.AI.MachineLearning`, WinML 2.x reg-free) These must be kept in sync with the cmake defaults and with diff --git a/sdk_v2/cpp/test/utils/temp_path.h b/sdk_v2/cpp/test/utils/temp_path.h index f5e8bbb98..8353ee6b4 100644 --- a/sdk_v2/cpp/test/utils/temp_path.h +++ b/sdk_v2/cpp/test/utils/temp_path.h @@ -37,7 +37,8 @@ inline long CurrentPid() { /// within one process, so no two live temp paths collide — no randomness required. inline std::filesystem::path MakeUniqueTempPath(const std::string& prefix) { static std::atomic counter{0}; - return std::filesystem::temp_directory_path() / + const auto temp_directory = std::filesystem::canonical(std::filesystem::temp_directory_path()); + return temp_directory / (prefix + std::to_string(CurrentPid()) + "_" + std::to_string(counter.fetch_add(1, std::memory_order_relaxed))); } diff --git a/sdk_v2/deps_versions.json b/sdk_v2/deps_versions.json index ca0174f5f..db735f041 100644 --- a/sdk_v2/deps_versions.json +++ b/sdk_v2/deps_versions.json @@ -1,6 +1,6 @@ { "_comment": "Single source of truth for native dependency versions in sdk_v2. Read by sdk_v2/cpp/cmake/Find*.cmake and sdk_v2/python/_build_backend/__init__.py. The .pipelines/foundry-local-packaging.yml literals must match; the 'Validate pinned versions' step fails the build on drift.", "onnxruntime": { "version": "1.28.0" }, - "onnxruntime-genai": { "version": "0.15.1" }, + "onnxruntime-genai": { "version": "0.15.2" }, "windows-ai-machinelearning": { "version": "2.1.70" } } diff --git a/sdk_v2/js/test/install-native.test.ts b/sdk_v2/js/test/install-native.test.ts index 1be992d8d..afc860d4d 100644 --- a/sdk_v2/js/test/install-native.test.ts +++ b/sdk_v2/js/test/install-native.test.ts @@ -244,10 +244,10 @@ describe("generateRestoreProjectXml", () => { it("includes bracketed exact versions for every artifact", () => { const xml = generateRestoreProjectXml([ { name: "Microsoft.ML.OnnxRuntime", version: "1.28.0", expected: "onnxruntime.dll" }, - { name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", version: "0.15.1", expected: "onnxruntime-genai.dll" }, + { name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", version: "0.15.2", expected: "onnxruntime-genai.dll" }, ]); expect(xml).toContain(''); - expect(xml).toContain(''); + expect(xml).toContain(''); }); it("targets net8.0", () => { @@ -364,13 +364,13 @@ describe("buildNugetInstallArgs", () => { it("passes -ConfigFile and no -Source args when a config file is set", () => { const args = buildNugetInstallArgs( { ...config, configFile: "NuGet.config" }, - { id: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", version: "0.15.1", outputDir: "pkgs" }, + { id: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", version: "0.15.2", outputDir: "pkgs" }, ); expect(args).toEqual([ "install", "Microsoft.ML.OnnxRuntimeGenAI.Foundry", "-Version", - "0.15.1", + "0.15.2", "-OutputDirectory", "pkgs", "-NonInteractive", @@ -425,9 +425,9 @@ describe("findNugetPackageDir", () => { }); it("matches case-insensitively regardless of the casing nuget.exe produced", () => { - const dir = join(outputDir, "microsoft.ml.onnxruntimegenai.foundry.0.15.1"); + const dir = join(outputDir, "microsoft.ml.onnxruntimegenai.foundry.0.15.2"); mkdirSync(dir, { recursive: true }); - expect(findNugetPackageDir(outputDir, "Microsoft.ML.OnnxRuntimeGenAI.Foundry", "0.15.1")).toBe(dir); + expect(findNugetPackageDir(outputDir, "Microsoft.ML.OnnxRuntimeGenAI.Foundry", "0.15.2")).toBe(dir); }); it("only looks at immediate children of outputDir, not nested dependency package folders", () => { From 6bcd759268822fc7641a9d47c43d6bf503d59eb6 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Thu, 6 Aug 2026 07:43:45 +0000 Subject: [PATCH 06/11] Address pr review comments --- sdk_v2/cpp/CMakeLists.txt | 6 +- .../src/ep_detection/cuda_ep_bootstrapper.cc | 10 +- .../src/ep_detection/cuda_ep_bootstrapper.h | 6 +- sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h | 2 + .../src/ep_detection/ep_bundle_installer.cc | 33 +++--- .../src/ep_detection/ep_bundle_installer.h | 9 +- sdk_v2/cpp/src/ep_detection/ep_utils.cc | 102 +++------------- sdk_v2/cpp/src/ep_detection/ep_utils.h | 45 ++----- .../cpp/src/ep_detection/nvml_gpu_detector.cc | 22 +++- .../cpp/src/ep_detection/nvml_gpu_detector.h | 4 +- .../ep_detection/webgpu_ep_bootstrapper.cc | 89 +++++++------- .../src/ep_detection/webgpu_ep_bootstrapper.h | 4 +- sdk_v2/cpp/src/http/http_download.h | 6 +- sdk_v2/cpp/src/manager.cc | 17 +-- .../internal_api/cuda_ep_bootstrapper_test.cc | 19 +-- .../internal_api/ep_bundle_installer_test.cc | 6 +- sdk_v2/cpp/test/internal_api/ep_utils_test.cc | 112 ++---------------- .../webgpu_ep_bootstrapper_test.cc | 4 +- 18 files changed, 163 insertions(+), 333 deletions(-) diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index d05efcfe4..29a3a5efd 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -264,9 +264,11 @@ function(foundry_local_configure_target TARGET LINK_SCOPE) if(WIN32) target_link_libraries(${TARGET} ${LINK_SCOPE} dbghelp bcrypt) # UWP builds have CMAKE_SYSTEM_NAME = "WindowsStore"; desktop = "Windows". - # WinHTTP is not available in UWP, so only define this on desktop Windows. if(NOT CMAKE_SYSTEM_NAME STREQUAL "WindowsStore") - target_compile_definitions(${TARGET} PRIVATE FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT=1) + target_compile_definitions(${TARGET} PRIVATE + FOUNDRY_LOCAL_DESKTOP_WINDOWS=1 + FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT=1 + ) endif() endif() diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index f92841101..e47ad727f 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -105,12 +105,14 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } - if (progress_cb && !progress_cb(name_, 90.0f)) { + if (progress_cb && !progress_cb(name_, kEpReadyToRegisterProgress)) { return false; } #ifdef _WIN32 - PrependDirToProcessPath(provider_path.parent_path()); + if (!search_path_owner_.Add(provider_path.parent_path(), "CUDA EP", logger)) { + return false; + } #endif #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) @@ -155,7 +157,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - if (!dependency_owner_.Load(txn->bin_dir(), *manifest, "CUDA EP", logger)) { + if (!search_path_owner_.Add(txn->bin_dir(), "CUDA EP", logger)) { return false; } #elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) @@ -183,7 +185,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& } } -bool CudaEpBootstrapper::HasNvidiaGpu() { return NvmlGpuDetector::HasNvidiaGpu(); } +bool CudaEpBootstrapper::HasNvidiaGpu(ILogger& logger) { return NvmlGpuDetector::HasNvidiaGpu(logger); } bool CudaEpBootstrapper::IsSupportedPlatform() { #if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index cb0ca2c15..8399d9607 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -32,7 +32,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) override; /// Check for an NVIDIA GPU with compute capability >= 5.0 using NVML. - static bool HasNvidiaGpu(); + static bool HasNvidiaGpu(ILogger& logger); /// Whether Foundry Local publishes a CUDA EP bundle for this platform. static bool IsSupportedPlatform(); @@ -43,7 +43,9 @@ class CudaEpBootstrapper : public IEpBootstrapper { int attempts_ = 0; EpRegistrationCallback register_ep_; EpBundleInstaller installer_; - EpBundleDependencyOwner dependency_owner_; +#ifdef _WIN32 + EpBundleSearchPathOwner search_path_owner_; +#endif #if defined(__linux__) && !defined(__ANDROID__) void* genai_cuda_handle_ = nullptr; #endif diff --git a/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h index 8414f1913..41dddd4f3 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h @@ -9,6 +9,8 @@ namespace fl { class ILogger; +inline constexpr float kEpReadyToRegisterProgress = 90.0f; + /// Interface for a single execution provider bootstrapper. /// Each bootstrapper manages discovery, download, and registration of one EP. class IEpBootstrapper { diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc index 751750a0c..580ac216d 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc @@ -253,8 +253,9 @@ bool EnsureManagedDirectory(const std::filesystem::path& path, std::string_view } if (ec || !std::filesystem::is_directory(status)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, path.string())); + logger.Log(LogLevel::Warning, fmt::format("{}: refusing unsafe managed directory '{}'. Clear the EP cache " + "directory '{}' and retry", + ep_display_name, path.string(), path.parent_path().string())); return false; } @@ -266,15 +267,17 @@ bool ValidateManagedDirectories(const std::filesystem::path& bundles_dir, const std::error_code ec; const auto bundles_status = std::filesystem::symlink_status(bundles_dir, ec); if (ec || !std::filesystem::is_directory(bundles_status)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, bundles_dir.string())); + logger.Log(LogLevel::Warning, fmt::format("{}: refusing unsafe managed directory '{}'. Clear the EP cache " + "directory '{}' and retry", + ep_display_name, bundles_dir.string(), bundles_dir.parent_path().string())); return false; } const auto staging_status = std::filesystem::symlink_status(staging_root, ec); if (ec || !std::filesystem::is_directory(staging_status)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: refusing unsafe managed directory '{}'", ep_display_name, staging_root.string())); + logger.Log(LogLevel::Warning, fmt::format("{}: refusing unsafe managed directory '{}'. Clear the EP cache " + "directory '{}' and retry", + ep_display_name, staging_root.string(), staging_root.parent_path().string())); return false; } @@ -511,8 +514,10 @@ bool InstallArchiveArtifact(const EpArtifactDownloadFn& download_fn, const EpBun for (const auto& file : artifact.extracted_files) { const auto file_path = extraction_dir / file.relative_path; if (!std::filesystem::is_regular_file(file_path, ec)) { - logger.Log(LogLevel::Warning, fmt::format("{}: artifact '{}' is missing expected extracted file '{}'", - ep_display_name, artifact.id, file.relative_path)); + logger.Log(LogLevel::Warning, + fmt::format("{}: expected extracted file '{}' from artifact '{}' is unavailable or is not a regular " + "file", + ep_display_name, file.relative_path, artifact.id)); return false; } @@ -618,12 +623,12 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( VerifyBundleDir(active_bin, manifest, ep_display_name_, logger)) { logger.Log(LogLevel::Information, fmt::format("{}: reusing verified bundle '{}'", ep_display_name_, manifest.bundle_id)); - if (!ReportProgress(progress_cb, ep_display_name_, 90.0f)) { + if (!ReportProgress(progress_cb, ep_display_name_, kEpReadyToRegisterProgress)) { return nullptr; } - return std::unique_ptr(new EpInstallTransaction( - std::move(lock), root_dir_, ep_display_name_, manifest, *active_generation, active_bin)); + return std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, + *active_generation, active_bin); } auto staging_dir = staging_root / GenerateUniqueId(); @@ -673,12 +678,12 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( logger.Log(LogLevel::Information, fmt::format("{}: installed bundle '{}'", ep_display_name_, manifest.bundle_id)); - if (!ReportProgress(progress_cb, ep_display_name_, 90.0f)) { + if (!ReportProgress(progress_cb, ep_display_name_, kEpReadyToRegisterProgress)) { return nullptr; } - auto transaction = std::unique_ptr(new EpInstallTransaction( - std::move(lock), root_dir_, ep_display_name_, manifest, generation_id, final_bundle_dir / "bin")); + auto transaction = std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, + generation_id, final_bundle_dir / "bin"); final_cleanup.Release(); return transaction; } catch (const std::exception& e) { diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h index 66cfc8362..9e0379de2 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h @@ -32,6 +32,9 @@ enum class EpBundleInstallPolicy { class EpInstallTransaction { public: + EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, + std::string ep_display_name, EpBundleManifest manifest, std::string generation_id, + std::filesystem::path bin_dir); ~EpInstallTransaction(); EpInstallTransaction(const EpInstallTransaction&) = delete; @@ -46,12 +49,6 @@ class EpInstallTransaction { bool CommitActive(ILogger& logger); private: - friend class EpBundleInstaller; - - EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, - std::string ep_display_name, EpBundleManifest manifest, std::string generation_id, - std::filesystem::path bin_dir); - std::unique_ptr lock_; std::filesystem::path root_dir_; std::string ep_display_name_; diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc index f4ba970f5..d79379a80 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -2,107 +2,43 @@ // Licensed under the MIT License. #include "ep_detection/ep_utils.h" +#ifdef _WIN32 #include "logger.h" -#include "util/string_utils.h" #include -#ifdef _WIN32 +#include + #define WIN32_LEAN_AND_MEAN #include #endif namespace fl { -namespace { - -// The provider library itself and the core ORT runtime are handled separately from bundle -// dependencies: the provider is loaded by `RegisterExecutionProviderLibrary`, and the core runtime is -// already loaded by the host process, so neither should be preloaded again here. -bool IsCoreRuntimeLibrary(const std::filesystem::path& filename) { - const auto name = filename.string(); - return CompareCaseInsensitive(name, "onnxruntime.dll") == 0 || - CompareCaseInsensitive(name, "onnxruntime-genai.dll") == 0; -} - -} // namespace - -void PrependDirToProcessPath([[maybe_unused]] const std::filesystem::path& dir) { #ifdef _WIN32 - DWORD len = GetEnvironmentVariableW(L"PATH", nullptr, 0); - std::wstring prev_path; - if (len > 0) { - prev_path.resize(len); - GetEnvironmentVariableW(L"PATH", prev_path.data(), len); - prev_path.resize(len - 1); // remove trailing null +EpBundleSearchPathOwner::~EpBundleSearchPathOwner() { + for (auto it = cookies_.rbegin(); it != cookies_.rend(); ++it) { + RemoveDllDirectory(*it); } - - std::wstring new_path = dir.wstring() + L";" + prev_path; - SetEnvironmentVariableW(L"PATH", new_path.c_str()); -#endif } -std::vector SelectEpBundleDependenciesToPreload(const std::filesystem::path& bin_dir, - const EpBundleManifest& manifest) { - std::vector dependencies; - const auto provider_filename = std::filesystem::path(manifest.provider_relative_path).filename().string(); - - for (const auto& artifact : manifest.artifacts) { - for (const auto& file : artifact.extracted_files) { - const auto path = std::filesystem::absolute(bin_dir / file.relative_path); - - if (CompareCaseInsensitive(path.extension().string(), ".dll") != 0 || - CompareCaseInsensitive(path.filename().string(), provider_filename) == 0 || - IsCoreRuntimeLibrary(path.filename())) { - continue; - } - - dependencies.push_back(path); - } +bool EpBundleSearchPathOwner::Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger) { + const auto absolute_directory = std::filesystem::absolute(directory).lexically_normal(); + if (std::find(directories_.begin(), directories_.end(), absolute_directory) != directories_.end()) { + return true; } - return dependencies; -} - -EpBundleDependencyOwner::~EpBundleDependencyOwner() { -#ifdef _WIN32 - for (auto it = handles_.rbegin(); it != handles_.rend(); ++it) { - FreeLibrary(static_cast(*it)); + auto* cookie = AddDllDirectory(absolute_directory.c_str()); + if (cookie == nullptr) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to add DLL search directory '{}' ({})", ep_name, + absolute_directory.string(), GetLastError())); + return false; } -#endif -} - -bool EpBundleDependencyOwner::Load([[maybe_unused]] const std::filesystem::path& bin_dir, - [[maybe_unused]] const EpBundleManifest& manifest, - [[maybe_unused]] std::string_view ep_name, [[maybe_unused]] ILogger& logger) { -#ifdef _WIN32 - const auto dependencies = SelectEpBundleDependenciesToPreload(bin_dir, manifest); - handles_.reserve(handles_.size() + dependencies.size()); - - std::vector loaded; - loaded.reserve(dependencies.size()); - - for (const auto& path : dependencies) { - auto* handle = - LoadLibraryExW(path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); - if (handle == nullptr) { - logger.Log(LogLevel::Warning, - fmt::format("{}: failed to load dependency '{}' ({})", ep_name, path.string(), GetLastError())); - - for (auto it = loaded.rbegin(); it != loaded.rend(); ++it) { - FreeLibrary(static_cast(*it)); - } - - return false; - } - - loaded.push_back(handle); - } - - handles_.insert(handles_.end(), loaded.begin(), loaded.end()); -#endif - + directories_.push_back(absolute_directory); + cookies_.push_back(cookie); + cookies_.push_back(cookie); return true; } +#endif } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h index abbe77f0f..5ddf99d82 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -2,8 +2,6 @@ // Licensed under the MIT License. #pragma once -#include "ep_detection/ep_bundle_manifest.h" - #include #include #include @@ -12,42 +10,23 @@ namespace fl { class ILogger; -/// Prepend @p dir to the process `PATH` environment variable for the lifetime of the process. -/// -/// EP provider libraries (CUDA, WebGPU) delay-load sibling dependency DLLs from their own directory, -/// and `RegisterExecutionProviderLibrary` loads the provider DLL eagerly. The directory must be on -/// `PATH` before registration so those dependencies are discoverable. This is a no-op on non-Windows -/// platforms. -/// -/// @param dir Directory to prepend to `PATH`. -void PrependDirToProcessPath(const std::filesystem::path& dir); - -/// Select the manifest-declared DLLs under @p bin_dir that should be preloaded before the EP provider -/// library is registered, excluding the provider library itself and the core ORT runtime libraries -/// (`onnxruntime.dll`, `onnxruntime-genai.dll`), matched case-insensitively. -/// -/// This selection logic is platform-independent (pure path/string manipulation) so it can be unit -/// tested on any platform, even though the actual preloading only happens on Windows. -/// -/// @param bin_dir Directory containing the extracted bundle files. -/// @param manifest Bundle manifest describing the extracted artifacts. -/// @return Absolute paths of the DLLs that should be preloaded, in manifest order. -std::vector SelectEpBundleDependenciesToPreload(const std::filesystem::path& bin_dir, - const EpBundleManifest& manifest); - -class EpBundleDependencyOwner { +#ifdef _WIN32 +/// Keeps EP bundle directories available to the Windows DLL loader for dependencies loaded after +/// provider registration. Provider libraries themselves are still loaded and owned by ORT. +class EpBundleSearchPathOwner { public: - EpBundleDependencyOwner() = default; - ~EpBundleDependencyOwner(); + EpBundleSearchPathOwner() = default; + ~EpBundleSearchPathOwner(); - EpBundleDependencyOwner(const EpBundleDependencyOwner&) = delete; - EpBundleDependencyOwner& operator=(const EpBundleDependencyOwner&) = delete; + EpBundleSearchPathOwner(const EpBundleSearchPathOwner&) = delete; + EpBundleSearchPathOwner& operator=(const EpBundleSearchPathOwner&) = delete; - bool Load(const std::filesystem::path& bin_dir, const EpBundleManifest& manifest, std::string_view ep_name, - ILogger& logger); + bool Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger); private: - std::vector handles_; + std::vector directories_; + std::vector cookies_; }; +#endif } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc index 8aca8957b..a3ef6fa90 100644 --- a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc +++ b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc @@ -2,9 +2,13 @@ // Licensed under the MIT License. #include "ep_detection/nvml_gpu_detector.h" +#include "logger.h" + +#include + #include -#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT) +#if defined(FOUNDRY_LOCAL_DESKTOP_WINDOWS) #define WIN32_LEAN_AND_MEAN #include #include @@ -25,7 +29,7 @@ using NvmlDeviceGetCountFn = int (*)(unsigned int*); using NvmlDeviceGetHandleByIndexFn = int (*)(unsigned int, NvmlDevice*); using NvmlDeviceGetCudaComputeCapabilityFn = int (*)(NvmlDevice, int*, int*); -#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT) +#if defined(FOUNDRY_LOCAL_DESKTOP_WINDOWS) using LibraryHandle = HMODULE; constexpr LibraryHandle kNullLibrary = nullptr; @@ -96,7 +100,7 @@ void UnloadLibrary(LibraryHandle lib) { class NvmlLibrary { public: - NvmlLibrary() { + explicit NvmlLibrary(ILogger& logger) { lib_ = LoadNvmlLibrary(); if (!lib_) { return; @@ -110,12 +114,18 @@ class NvmlLibrary { reinterpret_cast(GetSymbol(lib_, "nvmlDeviceGetCudaComputeCapability")); if (!init_ || !shutdown_ || !get_count_ || !get_handle_ || !get_compute_cap_) { + logger.Log(LogLevel::Warning, "NVML library is missing required symbols; CUDA detection is unavailable"); UnloadLibrary(lib_); lib_ = kNullLibrary; return; } - initialized_ = (init_() == kNvmlSuccess); + const auto init_result = init_(); + initialized_ = (init_result == kNvmlSuccess); + if (!initialized_) { + logger.Log(LogLevel::Warning, + fmt::format("NVML initialization failed with error {}; CUDA detection is unavailable", init_result)); + } } ~NvmlLibrary() { @@ -180,8 +190,8 @@ bool HasQualifyingComputeCapability(const std::vector>& capa return false; } -bool NvmlGpuDetector::HasNvidiaGpu() { - NvmlLibrary nvml; +bool NvmlGpuDetector::HasNvidiaGpu(ILogger& logger) { + NvmlLibrary nvml(logger); if (!nvml.IsReady()) { return false; } diff --git a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h index 2a59f8db9..689eb64d2 100644 --- a/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h +++ b/sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h @@ -7,13 +7,15 @@ namespace fl { +class ILogger; + bool HasQualifyingComputeCapability(const std::vector>& capabilities, int min_major = 5, int min_minor = 0); class NvmlGpuDetector { public: - static bool HasNvidiaGpu(); + static bool HasNvidiaGpu(ILogger& logger); }; } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index 06c613a8a..1f224e247 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -17,49 +17,54 @@ namespace { constexpr const char* kLockFileName = "webgpu-ep.lock"; constexpr int kMaxInstallAttempts = 5; -#if defined(_WIN32) && defined(_M_ARM64) -constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-arm64"; -constexpr const char* kDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-arm64.zip"; -constexpr const char* kArchiveSha256 = "3674C8BD50F19AB84D3F738AC426DB37EB119BC1B790525B5A4F4139C253AF08"; -constexpr const char* kProviderSha256 = "63CFEF0E7FB8FDC2238F69CD8E804F50FDA393B2B60C448DAEC73E031DE75058"; -constexpr const char* kDxCompilerSha256 = "3895C1F437E8E91A771F562AD2E5EA9EF918365EA1D7D4216AF4C58BA87E9D7B"; -constexpr const char* kDxilSha256 = "9377B286B378AF2ACD7DA7686F25FB60C7D22DEC4BA384BAB0523494DE3E75D0"; -#elif defined(_WIN32) && defined(_M_X64) -constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-x64"; -constexpr const char* kDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-x64.zip"; -constexpr const char* kArchiveSha256 = "91A05B2C9EAF326011FE74604BBDF06E08C5B95A1F40425F4426EF0E90A9984D"; -constexpr const char* kProviderSha256 = "BE2EBCC0A96D1558D9123C04E75C2851260FE45C9DBC8959CB2CD8D11B83ABBE"; -constexpr const char* kDxCompilerSha256 = "174DBC3DF8F7AF5C32C0E39F43C0D5BC576395EDC3CCDD64119A1B63C081ED55"; -constexpr const char* kDxilSha256 = "080C02F62E90D0AB7ACC463BBC10280C37397DC6D036224D7C10F2ED9C20E13D"; -#elif defined(__APPLE__) && defined(__aarch64__) -constexpr const char* kBundleId = "webgpu-ep-0.2.1-macos-arm64"; -constexpr const char* kDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_macos-arm64.zip"; -constexpr const char* kArchiveSha256 = "5F0F8378172F53EFA281328F33D1EBC793E44197FFB4CC605D02C593B891C0EA"; -constexpr const char* kProviderSha256 = "8FAC874A60F32F0127C74CB7DEF915807FCC8A6C30B77629E45F8CEE60272EAE"; -#endif constexpr const char* kRegistrationName = "Foundry.WebGPU"; constexpr const char* kWebGpuProviderOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; constexpr uint64_t kArchiveMaxBytes = 64ULL * 1024 * 1024; std::optional BuildWebGpuManifest() { -#if defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64)) +#if defined(_WIN32) && defined(_M_ARM64) + fl::EpBundleManifest manifest; + manifest.bundle_id = "webgpu-ep-0.2.1-win-arm64"; + manifest.provider_relative_path = "onnxruntime_providers_webgpu.dll"; + manifest.artifacts = {{ + .id = "webgpu-ep", + .url = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-arm64.zip", + .is_archive = true, + .archive_sha256 = "3674C8BD50F19AB84D3F738AC426DB37EB119BC1B790525B5A4F4139C253AF08", + .extracted_files = + { + {.relative_path = "dxcompiler.dll", + .sha256 = "3895C1F437E8E91A771F562AD2E5EA9EF918365EA1D7D4216AF4C58BA87E9D7B"}, + {.relative_path = "dxil.dll", + .sha256 = "9377B286B378AF2ACD7DA7686F25FB60C7D22DEC4BA384BAB0523494DE3E75D0"}, + {.relative_path = "onnxruntime_providers_webgpu.dll", + .sha256 = "63CFEF0E7FB8FDC2238F69CD8E804F50FDA393B2B60C448DAEC73E031DE75058"}, + }, + .ignored_archive_paths = {"version.json"}, + .archive_max_bytes = kArchiveMaxBytes, + .raw_relative_path = "", + .raw_sha256 = "", + .raw_max_bytes = 0, + }}; + return manifest; +#elif defined(_WIN32) && defined(_M_X64) fl::EpBundleManifest manifest; - manifest.bundle_id = kBundleId; + manifest.bundle_id = "webgpu-ep-0.2.1-win-x64"; manifest.provider_relative_path = "onnxruntime_providers_webgpu.dll"; manifest.artifacts = {{ .id = "webgpu-ep", - .url = kDownloadUrl, + .url = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-x64.zip", .is_archive = true, - .archive_sha256 = kArchiveSha256, + .archive_sha256 = "91A05B2C9EAF326011FE74604BBDF06E08C5B95A1F40425F4426EF0E90A9984D", .extracted_files = { - {.relative_path = "dxcompiler.dll", .sha256 = kDxCompilerSha256}, - {.relative_path = "dxil.dll", .sha256 = kDxilSha256}, - {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = kProviderSha256}, + {.relative_path = "dxcompiler.dll", + .sha256 = "174DBC3DF8F7AF5C32C0E39F43C0D5BC576395EDC3CCDD64119A1B63C081ED55"}, + {.relative_path = "dxil.dll", + .sha256 = "080C02F62E90D0AB7ACC463BBC10280C37397DC6D036224D7C10F2ED9C20E13D"}, + {.relative_path = "onnxruntime_providers_webgpu.dll", + .sha256 = "BE2EBCC0A96D1558D9123C04E75C2851260FE45C9DBC8959CB2CD8D11B83ABBE"}, }, .ignored_archive_paths = {"version.json"}, .archive_max_bytes = kArchiveMaxBytes, @@ -70,16 +75,17 @@ std::optional BuildWebGpuManifest() { return manifest; #elif defined(__APPLE__) && defined(__aarch64__) fl::EpBundleManifest manifest; - manifest.bundle_id = kBundleId; + manifest.bundle_id = "webgpu-ep-0.2.1-macos-arm64"; manifest.provider_relative_path = "libonnxruntime_providers_webgpu.dylib"; manifest.artifacts = {{ .id = "webgpu-ep", - .url = kDownloadUrl, + .url = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_macos-arm64.zip", .is_archive = true, - .archive_sha256 = kArchiveSha256, + .archive_sha256 = "5F0F8378172F53EFA281328F33D1EBC793E44197FFB4CC605D02C593B891C0EA", .extracted_files = { - {.relative_path = "libonnxruntime_providers_webgpu.dylib", .sha256 = kProviderSha256}, + {.relative_path = "libonnxruntime_providers_webgpu.dylib", + .sha256 = "8FAC874A60F32F0127C74CB7DEF915807FCC8A6C30B77629E45F8CEE60272EAE"}, }, .ignored_archive_paths = {"version.json"}, .archive_max_bytes = kArchiveMaxBytes, @@ -130,11 +136,15 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return false; } - if (progress_cb && !progress_cb(name_, 90.0f)) { + if (progress_cb && !progress_cb(name_, kEpReadyToRegisterProgress)) { return false; } - PrependDirToProcessPath(provider_path.parent_path()); +#ifdef _WIN32 + if (!search_path_owner_.Add(provider_path.parent_path(), "WebGPU EP", logger)) { + return false; + } +#endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: ORT registration failed for override {}={}", @@ -172,13 +182,10 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; #ifdef _WIN32 - // The provider can delay-load sibling DirectX compiler binaries after registration. - PrependDirToProcessPath(txn->bin_dir()); -#endif - - if (!dependency_owner_.Load(txn->bin_dir(), *manifest, "WebGPU EP", logger)) { + if (!search_path_owner_.Add(txn->bin_dir(), "WebGPU EP", logger)) { return false; } +#endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "WebGPU EP: ORT registration failed"); diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index c89841bba..7bc0ec613 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -40,7 +40,9 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { int attempts_ = 0; EpRegistrationCallback register_ep_; EpBundleInstaller installer_; - EpBundleDependencyOwner dependency_owner_; +#ifdef _WIN32 + EpBundleSearchPathOwner search_path_owner_; +#endif }; } // namespace fl diff --git a/sdk_v2/cpp/src/http/http_download.h b/sdk_v2/cpp/src/http/http_download.h index 018179568..12739326e 100644 --- a/sdk_v2/cpp/src/http/http_download.h +++ b/sdk_v2/cpp/src/http/http_download.h @@ -26,9 +26,9 @@ bool ContentLengthMatchesBody(int64_t content_length, int64_t bytes_downloaded); /// @param cancel_flag Set to true to cancel. nullptr if not needed. /// @param progress_cb Called with percent 0.0-100.0. Empty = no callback. /// @param logger Logger for diagnostic output on failure. -/// @param max_bytes Fail closed if a Content-Length header exceeds this, and abort mid-stream -/// if the body exceeds it regardless of what Content-Length promised (defends -/// against a missing/incorrect header on chunked transfers). -1 means no cap. +/// @param max_bytes Maximum accepted response size. This bounds disk and network usage if the +/// endpoint unexpectedly returns a larger body than the artifact manifest allows. +/// -1 means no cap. /// @return true on success, false on failure. bool HttpDownloadFile(const std::string& url, const std::filesystem::path& destination, const std::string& user_agent, std::atomic* cancel_flag, std::function progress_cb, ILogger& logger, diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index 0443ff742..0612b9745 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -242,7 +242,8 @@ Manager::Manager(const Configuration& config) : config_(config) { // Detected once and reused below for the WinML catalog skip-list and CUDA bootstrapper. // Avoid probing NVML on platforms where Foundry Local does not publish a CUDA bundle. - const bool has_nvidia_gpu = CudaEpBootstrapper::IsSupportedPlatform() && CudaEpBootstrapper::HasNvidiaGpu(); + const bool has_nvidia_gpu = + CudaEpBootstrapper::IsSupportedPlatform() && CudaEpBootstrapper::HasNvidiaGpu(*logger_); #if FOUNDRY_LOCAL_HAS_EP_CATALOG // WinML EPs — enumerate from the OS EP catalog (Windows 10 19H1+ reg-free runtime). @@ -314,11 +315,8 @@ Manager::~Manager() { try { Shutdown(); } catch (const std::exception& e) { - try { - safe_log(LogLevel::Error, - std::string("Exception while shutting down Manager subsystems during destruction: ") + e.what()); - } catch (...) { - } + safe_log(LogLevel::Error, + std::string("Exception while shutting down Manager subsystems during destruction: ") + e.what()); } catch (...) { safe_log(LogLevel::Error, "Unknown exception while shutting down Manager subsystems during destruction."); } @@ -340,11 +338,8 @@ Manager::~Manager() { OrtStatus* status = ort_api_->UnregisterExecutionProviderLibrary(ort_env_, name.c_str()); if (status != nullptr) { const char* msg = ort_api_->GetErrorMessage(status); - try { - safe_log(LogLevel::Warning, std::string("EP unregister: UnregisterExecutionProviderLibrary failed for '") + - name + "': " + (msg ? msg : "unknown")); - } catch (...) { - } + safe_log(LogLevel::Warning, std::string("EP unregister: UnregisterExecutionProviderLibrary failed for '") + + name + "': " + (msg ? msg : "unknown")); ort_api_->ReleaseStatus(status); } } diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index 05e1b46dd..59dec1f56 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -166,23 +166,6 @@ TEST(CudaEpManifestTest, UnsupportedPlatformsHaveNoManifest) { EXPECT_FALSE(BuildCudaEpManifest(CudaEpPlatform::Unsupported).has_value()); } -TEST(CudaEpManifestTest, WindowsPreloadIncludesRuntimeDependenciesButExcludesProviderAndCoreOrt) { - const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsX64); - ASSERT_TRUE(manifest.has_value()); - - const auto dependencies = SelectEpBundleDependenciesToPreload("bin", *manifest); - std::vector filenames; - std::transform(dependencies.begin(), dependencies.end(), std::back_inserter(filenames), - [](const auto& path) { return path.filename().string(); }); - - EXPECT_NE(std::find(filenames.begin(), filenames.end(), "cudart64_12.dll"), filenames.end()); - EXPECT_NE(std::find(filenames.begin(), filenames.end(), "cudnn64_9.dll"), filenames.end()); - EXPECT_NE(std::find(filenames.begin(), filenames.end(), "onnxruntime-genai-cuda.dll"), filenames.end()); - EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime_providers_cuda.dll"), filenames.end()); - EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime.dll"), filenames.end()); - EXPECT_EQ(std::find(filenames.begin(), filenames.end(), "onnxruntime-genai.dll"), filenames.end()); -} - TEST(CudaEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) { auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); const auto provider_path = root.path() / "custom_cuda_provider"; @@ -197,7 +180,7 @@ TEST(CudaEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) StderrLogger logger; EXPECT_FALSE(bootstrapper.DownloadAndRegister( - false, [](const std::string&, float percent) { return percent != 90.0f; }, logger)); + false, [](const std::string&, float percent) { return percent != kEpReadyToRegisterProgress; }, logger)); EXPECT_FALSE(bootstrapper.IsRegistered()); EXPECT_EQ(registration_count, 0); } diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc index 5d9a54632..90fb971f7 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -830,7 +830,9 @@ TEST(EpBundleInstallerTest, CancellationBeforeReturningNewTransactionRemovesPubl EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - auto cancel_before_transaction = [](const std::string&, float percent) { return percent < 90.0f; }; + auto cancel_before_transaction = [](const std::string&, float percent) { + return percent < kEpReadyToRegisterProgress; + }; EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_before_transaction, logger), nullptr); EXPECT_TRUE(std::filesystem::is_empty(root.path() / "staging")); @@ -849,7 +851,7 @@ TEST(EpBundleInstallerTest, CancellationOnVerifiedBundleReuseReturnsNoTransactio ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); const auto active_generation = ReadFile(root.path() / "active"); - auto cancel_reuse = [](const std::string&, float percent) { return percent != 90.0f; }; + auto cancel_reuse = [](const std::string&, float percent) { return percent != kEpReadyToRegisterProgress; }; EXPECT_EQ(installer.EnsureInstalled(manifest, cancel_reuse, logger), nullptr); EXPECT_EQ(downloads.CallCount("https://example.test/provider.so"), 1); diff --git a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc index 4a120f2de..cdb2dcde0 100644 --- a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc @@ -2,11 +2,12 @@ // Licensed under the MIT License. #include "ep_detection/ep_utils.h" +#ifdef _WIN32 #include "logger.h" +#include "utils/temp_path.h" #include -#include #include namespace fl { @@ -18,113 +19,16 @@ class NullLogger : public ILogger { void Log(LogLevel /*level*/, std::string_view /*message*/) override {} }; -// A representative manifest mixing the provider DLL, non-DLL manifest entries, core ORT runtime DLLs -// (in mixed case, to exercise case-insensitive exclusion), and DLL dependencies that should be -// preloaded, spread across more than one artifact. -EpBundleManifest MakeCudaLikeManifest() { - EpBundleManifest manifest; - manifest.bundle_id = "test-cuda-ep"; - manifest.provider_relative_path = "onnxruntime_providers_cuda.dll"; - manifest.artifacts = { - EpBundleArtifact{ - .id = "cuda-ep", - .url = "https://example.test/cuda-ep.zip", - .is_archive = true, - .archive_sha256 = "archive-hash", - .extracted_files = - { - {.relative_path = "onnxruntime_providers_cuda.dll", .sha256 = "a"}, - {.relative_path = "cudart64_12.dll", .sha256 = "b"}, - {.relative_path = "ONNXRUNTIME.DLL", .sha256 = "c"}, - {.relative_path = "onnxruntime-genai.dll", .sha256 = "d"}, - }, - .ignored_archive_paths = {"version.json"}, - .archive_max_bytes = 0, - .raw_relative_path = "", - .raw_sha256 = "", - .raw_max_bytes = 0, - }, - EpBundleArtifact{ - .id = "cuda-toolkit", - .url = "https://example.test/cuda-toolkit.zip", - .is_archive = true, - .archive_sha256 = "archive-hash", - .extracted_files = - { - {.relative_path = "cublas64_12.DLL", .sha256 = "f"}, - }, - .ignored_archive_paths = {}, - .archive_max_bytes = 0, - .raw_relative_path = "", - .raw_sha256 = "", - .raw_max_bytes = 0, - }, - }; - return manifest; -} - } // namespace -TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadExcludesProviderAndCoreRuntimeCaseInsensitively) { - const std::filesystem::path bin_dir = std::filesystem::path("opt") / "foundry" / "cuda-ep" / "bin"; - const auto manifest = MakeCudaLikeManifest(); - - const auto dependencies = SelectEpBundleDependenciesToPreload(bin_dir, manifest); - - // Only the two non-provider, non-core-runtime DLLs should remain, in manifest order. Ignored metadata is - // not considered, and provider/core runtime DLLs are excluded case-insensitively. - ASSERT_EQ(dependencies.size(), 2u); - EXPECT_EQ(dependencies[0], std::filesystem::absolute(bin_dir / "cudart64_12.dll")); - EXPECT_EQ(dependencies[1], std::filesystem::absolute(bin_dir / "cublas64_12.DLL")); - EXPECT_TRUE(dependencies[0].is_absolute()); - EXPECT_TRUE(dependencies[1].is_absolute()); -} - -TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadReturnsEmptyWhenOnlyProviderAndCoreRuntimePresent) { - EpBundleManifest manifest; - manifest.bundle_id = "test-webgpu-ep"; - manifest.provider_relative_path = "onnxruntime_providers_webgpu.dll"; - manifest.artifacts = { - EpBundleArtifact{ - .id = "webgpu-ep", - .url = "https://example.test/webgpu-ep.zip", - .is_archive = true, - .archive_sha256 = "archive-hash", - .extracted_files = - { - {.relative_path = "onnxruntime_providers_webgpu.dll", .sha256 = "a"}, - {.relative_path = "onnxruntime.dll", .sha256 = "b"}, - }, - .ignored_archive_paths = {"version.json"}, - .archive_max_bytes = 0, - .raw_relative_path = "", - .raw_sha256 = "", - .raw_max_bytes = 0, - }, - }; - - EXPECT_TRUE(SelectEpBundleDependenciesToPreload("bin", manifest).empty()); -} - -TEST(EpUtilsTest, SelectEpBundleDependenciesToPreloadHandlesManifestWithNoArtifacts) { - EpBundleManifest manifest; - manifest.bundle_id = "empty"; - manifest.provider_relative_path = "provider.dll"; - - EXPECT_TRUE(SelectEpBundleDependenciesToPreload("bin", manifest).empty()); -} - -TEST(EpUtilsTest, DependencyOwnerLoadIsNoOpOnNonWindows) { -#ifndef _WIN32 +TEST(EpUtilsTest, SearchPathOwnerAcceptsExistingDirectoryAndDuplicateAdd) { + auto directory = test::TempPath::CreateTempDir("fl_ep_search_path_"); NullLogger logger; - const auto manifest = MakeCudaLikeManifest(); - EpBundleDependencyOwner owner; + EpBundleSearchPathOwner owner; - EXPECT_TRUE(owner.Load("nonexistent/bin/dir", manifest, "Test EP", logger)); - EXPECT_TRUE(owner.Load("another/nonexistent/bin/dir", manifest, "Test EP", logger)); -#else - GTEST_SKIP() << "Preloading behavior is exercised on Windows only."; -#endif + EXPECT_TRUE(owner.Add(directory.path(), "Test EP", logger)); + EXPECT_TRUE(owner.Add(directory.path(), "Test EP", logger)); } } // namespace fl +#endif diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc index 6fec4fddb..f83d3dca9 100644 --- a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -86,7 +86,7 @@ TEST(WebGpuEpBootstrapperTest, OverrideRegistersUsingExistingProviderConvention) EXPECT_EQ(registered_path, std::filesystem::absolute(provider_path)); EXPECT_EQ(registration_count, 1); ASSERT_EQ(progress.size(), 2u); - EXPECT_EQ(progress[0], std::make_pair(std::string("WebGpuExecutionProvider"), 90.0f)); + EXPECT_EQ(progress[0], std::make_pair(std::string("WebGpuExecutionProvider"), kEpReadyToRegisterProgress)); EXPECT_EQ(progress[1], std::make_pair(std::string("WebGpuExecutionProvider"), 100.0f)); progress.clear(); @@ -117,7 +117,7 @@ TEST(WebGpuEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFals StderrLogger logger; EXPECT_FALSE(bootstrapper.DownloadAndRegister( - false, [](const std::string&, float percent) { return percent != 90.0f; }, logger)); + false, [](const std::string&, float percent) { return percent != kEpReadyToRegisterProgress; }, logger)); EXPECT_FALSE(bootstrapper.IsRegistered()); EXPECT_EQ(registration_count, 0); } From e22a798009d364372a8035c3d7916517f7d422db Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Thu, 6 Aug 2026 20:17:43 +0000 Subject: [PATCH 07/11] Address failure and update cuda ep --- .../src/ep_detection/cuda_ep_bootstrapper.cc | 107 ++++- .../src/ep_detection/cuda_ep_bootstrapper.h | 23 +- .../cpp/src/ep_detection/cuda_ep_manifest.cc | 46 +- .../src/ep_detection/ep_bundle_installer.cc | 132 +++++- .../src/ep_detection/ep_bundle_installer.h | 16 +- sdk_v2/cpp/src/ep_detection/ep_utils.cc | 42 +- sdk_v2/cpp/src/ep_detection/ep_utils.h | 2 + .../ep_detection/webgpu_ep_bootstrapper.cc | 34 +- .../src/ep_detection/webgpu_ep_bootstrapper.h | 5 +- .../internal_api/cuda_ep_bootstrapper_test.cc | 424 ++++++++++++++++-- .../internal_api/ep_bundle_installer_test.cc | 259 ++++++++++- .../webgpu_ep_bootstrapper_test.cc | 225 ++++++++++ 12 files changed, 1191 insertions(+), 124 deletions(-) diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index e47ad727f..8a2121a84 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -10,8 +10,10 @@ #include +#include #include #include +#include #if defined(__linux__) && !defined(__ANDROID__) #include @@ -42,20 +44,42 @@ fl::CudaEpPlatform HostCudaEpPlatform() { } #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) -bool LoadGenAiCudaLibrary(const std::filesystem::path& path, void*& handle, fl::ILogger& logger) { - if (handle) { - return true; - } - +std::shared_ptr DefaultGenAiCudaLoader(const std::filesystem::path& path, fl::ILogger& logger) { dlerror(); - handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL | RTLD_NODELETE); + void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL); if (!handle) { const char* error = dlerror(); logger.Log(fl::LogLevel::Warning, fmt::format("CUDA EP: failed to load '{}' ({})", path.string(), error ? error : "unknown error")); + return {}; + } + + return std::shared_ptr(handle, [](void* loaded_library) { + if (loaded_library != nullptr) { + dlclose(loaded_library); + } + }); +} + +bool LoadGenAiCudaLibrary( + const std::filesystem::path& path, + const std::vector>>& loaded_libraries, + const fl::CudaGenAiDependencyLoader& loader, + std::pair>& provisional_library, fl::ILogger& logger) { + const auto absolute_path = std::filesystem::absolute(path).lexically_normal(); + const auto already_loaded = + std::any_of(loaded_libraries.begin(), loaded_libraries.end(), + [&](const auto& loaded_library) { return loaded_library.first == absolute_path; }); + if (already_loaded) { + return true; + } + + auto loaded_library = loader ? loader(absolute_path, logger) : DefaultGenAiCudaLoader(absolute_path, logger); + if (!loaded_library) { return false; } + provisional_library = {absolute_path, std::move(loaded_library)}; return true; } #endif @@ -64,17 +88,26 @@ bool LoadGenAiCudaLibrary(const std::filesystem::path& path, void*& handle, fl:: namespace fl { -CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) - : register_ep_(std::move(register_ep)), installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP") {} - -CudaEpBootstrapper::~CudaEpBootstrapper() { -#if defined(__linux__) && !defined(__ANDROID__) - if (genai_cuda_handle_) { - dlclose(genai_cuda_handle_); - } +CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, + EpBundleManifestFactory manifest_factory, EpArtifactDownloadFn download_fn +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + , + CudaGenAiDependencyLoader genai_cuda_loader #endif + ) + : register_ep_(std::move(register_ep)), + manifest_factory_(manifest_factory ? std::move(manifest_factory) + : [] { return BuildCudaEpManifest(HostCudaEpPlatform()); }), + installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP", std::move(download_fn)) +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + , + genai_cuda_loader_(genai_cuda_loader ? std::move(genai_cuda_loader) : DefaultGenAiCudaLoader) +#endif +{ } +CudaEpBootstrapper::~CudaEpBootstrapper() = default; + const std::string& CudaEpBootstrapper::Name() const { return name_; } bool CudaEpBootstrapper::IsRegistered() const { return registered_; } @@ -110,13 +143,17 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& } #ifdef _WIN32 - if (!search_path_owner_.Add(provider_path.parent_path(), "CUDA EP", logger)) { + EpBundleSearchPathOwner provisional_search_path_owner; + if (!search_path_owner_.Owns(provider_path.parent_path()) && + !provisional_search_path_owner.Add(provider_path.parent_path(), "CUDA EP", logger)) { return false; } #endif #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) - if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_handle_, logger)) { + std::pair> provisional_genai_cuda_library; + if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_libraries_, + genai_cuda_loader_, provisional_genai_cuda_library, logger)) { return false; } #endif @@ -127,6 +164,16 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } +#ifdef _WIN32 + search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); +#endif + +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + if (provisional_genai_cuda_library.second) { + genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); + } +#endif + registered_ = true; if (progress_cb) { @@ -138,7 +185,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return true; } - auto manifest = BuildCudaEpManifest(HostCudaEpPlatform()); + auto manifest = manifest_factory_(); if (!manifest.has_value()) { logger.Log(LogLevel::Warning, "CUDA EP: no bundle available for this platform"); return false; @@ -150,28 +197,44 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } - if (!txn->CommitActive(logger)) { - logger.Log(LogLevel::Warning, "CUDA EP: failed to publish active bundle marker"); + if (!txn->Activate(logger)) { + logger.Log(LogLevel::Warning, "CUDA EP: failed to activate bundle"); return false; } - const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + const auto provider_path = txn->provider_path(); #ifdef _WIN32 - if (!search_path_owner_.Add(txn->bin_dir(), "CUDA EP", logger)) { + EpBundleSearchPathOwner provisional_search_path_owner; + if (!search_path_owner_.Owns(txn->bin_dir()) && + !provisional_search_path_owner.Add(txn->bin_dir(), "CUDA EP", logger)) { + txn->Rollback(logger); return false; } #elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) - if (!LoadGenAiCudaLibrary(txn->bin_dir() / kGenAiCudaLibrary, genai_cuda_handle_, logger)) { + std::pair> provisional_genai_cuda_library; + if (!LoadGenAiCudaLibrary(txn->bin_dir() / kGenAiCudaLibrary, genai_cuda_libraries_, genai_cuda_loader_, + provisional_genai_cuda_library, logger)) { + txn->Rollback(logger); return false; } #endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "CUDA EP: ORT registration failed"); + txn->Rollback(logger); return false; } +#ifdef _WIN32 + search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); +#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + if (provisional_genai_cuda_library.second) { + genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); + } +#endif + registered_ = true; + txn->Finalize(logger); if (progress_cb) { progress_cb(name_, 100.0f); diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index 8399d9607..79f8de8aa 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -7,12 +7,20 @@ #include "ep_detection/ep_types.h" #include "ep_detection/ep_utils.h" +#include #include +#include +#include namespace fl { class ILogger; +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +using CudaGenAiDependencyLoader = + std::function(const std::filesystem::path&, ILogger&)>; +#endif + /// Bootstrapper for the CUDA execution provider. /// /// Installs and registers the CUDA execution provider. @@ -20,7 +28,14 @@ class CudaEpBootstrapper : public IEpBootstrapper { public: /// @param root_dir Root directory for the CUDA EP bundle, e.g. "/ep/cuda-ep". /// @param register_ep Callback to register the EP DLL with ORT. - CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep); + CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, + EpBundleManifestFactory manifest_factory = nullptr, + EpArtifactDownloadFn download_fn = nullptr +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + , + CudaGenAiDependencyLoader genai_cuda_loader = nullptr +#endif + ); ~CudaEpBootstrapper() override; // Non-copyable @@ -42,12 +57,16 @@ class CudaEpBootstrapper : public IEpBootstrapper { bool registered_ = false; int attempts_ = 0; EpRegistrationCallback register_ep_; + EpBundleManifestFactory manifest_factory_; EpBundleInstaller installer_; #ifdef _WIN32 EpBundleSearchPathOwner search_path_owner_; #endif #if defined(__linux__) && !defined(__ANDROID__) - void* genai_cuda_handle_ = nullptr; + std::vector>> genai_cuda_libraries_; +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + CudaGenAiDependencyLoader genai_cuda_loader_; +#endif #endif }; diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc index aca40a31c..1c509beaf 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc @@ -32,12 +32,12 @@ EpBundleArtifact Archive(std::string id, std::string filename, std::string sha25 EpBundleManifest WindowsX64Manifest() { return EpBundleManifest{ - .bundle_id = "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260805-050438", + .bundle_id = "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260806-182620", .artifacts = { Archive( - "cuda-toolkit", "cuda-bins-win-x64-20260805-050438.zip", - "b47716cbd9a1c92722a6bc914ca57b0e8efea15f7b1a46eecfb2637cadc1bee5", 640 * kMiB, + "cuda-toolkit", "cuda-bins-win-x64-20260806-182620.zip", + "a90223e4091cfa63b1e40af27a5d5f0267fdfdd15f0459c2922106afe352d306", 640 * kMiB, { {.relative_path = "cublas64_12.dll", .sha256 = "9513540e4ec4c51ee9e7304138c2cc255c29a8c181f9e80c38efa25738becd99"}, @@ -47,8 +47,8 @@ EpBundleManifest WindowsX64Manifest() { .sha256 = "c2c9a9c22a9bcba90e261825968836787b331038047a26770cffb7a583c28344"}, }), Archive( - "cudnn", "cudnn-bins-win-x64-20260805-050438.zip", - "1b065e115c2ac35040053ebe594a8c089906f8cbe5b8d8ed832ba5eb27cdeb5e", 704 * kMiB, + "cudnn", "cudnn-bins-win-x64-20260806-182620.zip", + "b82cd271c8c9cbd52ea9e4dedaa4cc3864bf8d7b221d87d5bde81d6ee4a399da", 704 * kMiB, { {.relative_path = "cudnn64_9.dll", .sha256 = "0d1d71325eb5e91570ab8ba8e399e07bf717ffd76511b2407229a8f45e0b1305"}, @@ -66,13 +66,13 @@ EpBundleManifest WindowsX64Manifest() { .sha256 = "49487537744256a3d4365c4792b03bf31130ad1faea0a13eafa219620941d837"}, }), Archive( - "cuda-ep", "cuda-ep-bins-win-x64-20260805-050438.zip", - "65044a715a2d4b74e77f019988f77c936f5b62973c27cf2d59704cf39057e567", 256 * kMiB, + "cuda-ep", "cuda-ep-bins-win-x64-20260806-182620.zip", + "e62938987e848a0fbb3d215dfefaed40307d2446393909927ba0345eaaf3d263", 256 * kMiB, { {.relative_path = "onnxruntime-genai-cuda.dll", - .sha256 = "612ad6cf3d099431af886537080223a62522e58caba0c7d278b9b4b1eb03c4ce"}, + .sha256 = "7894fb5efaad4a663e834f20b912b44cc383629b24ffe8bbc6382786a7326dbc"}, {.relative_path = "onnxruntime_providers_cuda.dll", - .sha256 = "971c1002ce7c16338273f693316bec4862ac74c7efa2ffbd644630cfe10d6e37"}, + .sha256 = "60f1aeef7ebe27f7e659cb88f597005ca5a5e75832b85dcef3eef02b9322df9a"}, }), }, .provider_relative_path = "onnxruntime_providers_cuda.dll", @@ -81,12 +81,12 @@ EpBundleManifest WindowsX64Manifest() { EpBundleManifest WindowsArm64Manifest() { return EpBundleManifest{ - .bundle_id = "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260805-050639", + .bundle_id = "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260806-182803", .artifacts = { Archive( - "cuda-toolkit", "cuda-bins-win-arm64-20260805-050639.zip", - "b4e0ce6beea87843d02c7d41e04fee3d1a9fb22e0f4fb5e587914cf4f4b94113", 192 * kMiB, + "cuda-toolkit", "cuda-bins-win-arm64-20260806-182803.zip", + "de71001db47deb1b59567c50cd5fb1c7705945a9461c95505e987fb8731d6175", 192 * kMiB, { {.relative_path = "cublas64_13.dll", .sha256 = "80b322ce3fe77d1c6c0348e30a31c5f2682da4197680177a179af69275b57997"}, @@ -96,8 +96,8 @@ EpBundleManifest WindowsArm64Manifest() { .sha256 = "32504bd5f424a4e73d3bb5ecc69f018538ae371efa0210bd33e88c7c78b9dca7"}, }), Archive( - "cudnn", "cudnn-bins-win-arm64-20260805-050639.zip", - "24347fc6b596ae28c32659c82da688bc386da36228e65329df031c028d8527ad", 192 * kMiB, + "cudnn", "cudnn-bins-win-arm64-20260806-182803.zip", + "84338552f83a602e989e2a964ed37c342560486031c955225d265402ccf02bd1", 192 * kMiB, { {.relative_path = "cudnn64_9.dll", .sha256 = "247cecbb33132c829c6ed328b7dd34d077a27d0f0fb0ee0b56469ec6bdfd1c17"}, @@ -115,13 +115,13 @@ EpBundleManifest WindowsArm64Manifest() { .sha256 = "c9e0ec0e0a4e659393e15897ed1f6e5bac677e0c0fe7e12290f0386f19477b6b"}, }), Archive( - "cuda-ep", "cuda-ep-bins-win-arm64-20260805-050639.zip", - "8152d03a0fbef39bd11f5b07dbc6776abd125dbee1dc1d2877a04dc62bbde641", 96 * kMiB, + "cuda-ep", "cuda-ep-bins-win-arm64-20260806-182803.zip", + "212e670c61b3292d4a7d98f16fc2cf61f7b080604e0c145e81c39ec81e7b3259", 96 * kMiB, { {.relative_path = "onnxruntime-genai-cuda.dll", - .sha256 = "5284fdec9d4e9e25d6b4cf129205f0c88d3c2f5e678907b2bc1581b575266016"}, + .sha256 = "ab61145f4bc6284286e663586f634b973072d58ced20c497c7e5259f2ef3fc08"}, {.relative_path = "onnxruntime_providers_cuda.dll", - .sha256 = "b60cd5a26bc180229c9da0dc635d6b3404c306246708291dfae7c9f72ad5e862"}, + .sha256 = "d92ffbd23a84f91b976baed9031de267efe1dc892d85c09d0979d25b89f5d1a0"}, }), }, .provider_relative_path = "onnxruntime_providers_cuda.dll", @@ -130,17 +130,17 @@ EpBundleManifest WindowsArm64Manifest() { EpBundleManifest LinuxX64Manifest() { return EpBundleManifest{ - .bundle_id = "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260805-050706", + .bundle_id = "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260806-182830", .artifacts = { Archive( - "cuda-ep", "cuda-ep-linux-x64-20260805-050706.zip", - "2bc3e5949b75d7521d903c958716c06602ddaa5c2a1f98bd12811294db738c37", 448 * kMiB, + "cuda-ep", "cuda-ep-linux-x64-20260806-182830.zip", + "abf347e7234d7434105efde12a2e0609fdd1d8828167b9873f4463926f1206e6", 448 * kMiB, { {.relative_path = "libonnxruntime-genai-cuda.so", - .sha256 = "8b26db7a085de61653ebaaa8fc221b720879fe74583eb01204f11bf22638c345"}, + .sha256 = "d5300fc4413d9e74bd8dfceb5233fca6fcfa1d5ddc247081365fdb5f143091e6"}, {.relative_path = "libonnxruntime_providers_cuda.so", - .sha256 = "da94d951b89dc84c44b10f7faf52b17e675b3f1a13d8f32808264d425d0464bd"}, + .sha256 = "b88d7b7f4b2e81d3eff41663fc70f4ae9e03dee9e2301cb53dc250e5a96d7f7a"}, }), }, .provider_relative_path = "libonnxruntime_providers_cuda.so", diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc index 580ac216d..322af21fd 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc @@ -40,6 +40,11 @@ namespace { constexpr int kArchiveHashRetries = 1; constexpr int kRawHashRetries = 0; +class NoOpLogger : public ILogger { + public: + void Log(LogLevel /*level*/, std::string_view /*message*/) override {} +}; + class ScopedDirectoryCleanup { public: explicit ScopedDirectoryCleanup(std::filesystem::path path) : path_(std::move(path)) {} @@ -129,6 +134,32 @@ bool PublishActiveMarker(const std::filesystem::path& root_dir, const std::strin return true; } +bool RemoveActiveMarkerIfStillCandidate(const std::filesystem::path& root_dir, const std::string& generation_id, + std::string_view ep_display_name, ILogger& logger) { + const auto active_path = root_dir / "active"; + const auto active_generation = ReadActiveMarker(active_path); + if (!active_generation.has_value() || *active_generation != generation_id) { + return true; + } + + std::error_code ec; + if (!std::filesystem::remove(active_path, ec) && ec) { + logger.Log(LogLevel::Warning, fmt::format("{}: failed to remove active marker '{}': {}", ep_display_name, + active_path.string(), ec.message())); + return false; + } + + return true; +} + +void LogRollbackRecoveryError(std::string_view ep_display_name, const std::filesystem::path& root_dir, + ILogger& logger) { + logger.Log(LogLevel::Warning, + fmt::format("{}: failed to recover the active bundle marker after bootstrap failure. Clear the EP cache " + "directory '{}' and retry", + ep_display_name, root_dir.string())); +} + bool IsSha256(std::string_view value) { return value.size() == 64 && std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isxdigit(ch) != 0; }); @@ -628,7 +659,7 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( } return std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, - *active_generation, active_bin); + *active_generation, active_bin, active_generation); } auto staging_dir = staging_root / GenerateUniqueId(); @@ -683,7 +714,8 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( } auto transaction = std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, - generation_id, final_bundle_dir / "bin"); + generation_id, final_bundle_dir / "bin", + active_generation); final_cleanup.Release(); return transaction; } catch (const std::exception& e) { @@ -694,18 +726,26 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( EpInstallTransaction::EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, std::string ep_display_name, EpBundleManifest manifest, - std::string generation_id, std::filesystem::path bin_dir) + std::string generation_id, std::filesystem::path bin_dir, + std::optional previous_active_generation) : lock_(std::move(lock)), root_dir_(std::move(root_dir)), ep_display_name_(std::move(ep_display_name)), manifest_(std::move(manifest)), generation_id_(std::move(generation_id)), - bin_dir_(std::move(bin_dir)) {} + bin_dir_(std::move(bin_dir)), + previous_active_generation_(std::move(previous_active_generation)) {} + +EpInstallTransaction::~EpInstallTransaction() noexcept { + if (!activated_ || finalized_) { + return; + } -EpInstallTransaction::~EpInstallTransaction() = default; + (void)RollbackInternal(/*logger=*/nullptr, /*log_recovery_error=*/false); +} -bool EpInstallTransaction::CommitActive(ILogger& logger) { - if (committed_) { +bool EpInstallTransaction::Activate(ILogger& logger) { + if (activated_ || finalized_) { return true; } @@ -723,15 +763,87 @@ bool EpInstallTransaction::CommitActive(ILogger& logger) { return false; } + if (previous_active_generation_.has_value() && *previous_active_generation_ == generation_id_) { + activated_ = true; + return true; + } + if (!PublishActiveMarker(root_dir_, generation_id_, ep_display_name_, logger)) { return false; } - committed_ = true; - CleanupStaleGenerations(bundles_dir, staging_root, {generation_id_}, ep_display_name_, logger); + activated_ = true; return true; } catch (const std::exception& e) { - logger.Log(LogLevel::Warning, fmt::format("{}: failed to commit active bundle: {}", ep_display_name_, e.what())); + logger.Log(LogLevel::Warning, fmt::format("{}: failed to activate bundle: {}", ep_display_name_, e.what())); + return false; + } +} + +void EpInstallTransaction::Finalize(ILogger& logger) { + if (finalized_ || !activated_) { + return; + } + + finalized_ = true; + + try { + const auto bundles_dir = root_dir_ / "bundles"; + const auto staging_root = root_dir_ / "staging"; + (void)CleanupStaleGenerations(bundles_dir, staging_root, {generation_id_}, ep_display_name_, logger); + } catch (const std::exception& e) { + logger.Log(LogLevel::Warning, + fmt::format("{}: failed to clean stale bundle generations after activation: {}", ep_display_name_, + e.what())); + } +} + +bool EpInstallTransaction::Rollback(ILogger& logger) noexcept { + if (!activated_ || finalized_) { + return true; + } + + return RollbackInternal(&logger, /*log_recovery_error=*/true); +} + +bool EpInstallTransaction::RollbackInternal(ILogger* logger, bool log_recovery_error) noexcept { + if (!activated_ || finalized_) { + return true; + } + + NoOpLogger no_op_logger; + auto& active_logger = logger == nullptr ? static_cast(no_op_logger) : *logger; + + try { + bool rollback_succeeded = true; + if (previous_active_generation_.has_value()) { + if (*previous_active_generation_ != generation_id_) { + rollback_succeeded = + PublishActiveMarker(root_dir_, *previous_active_generation_, ep_display_name_, active_logger); + } + } else { + rollback_succeeded = + RemoveActiveMarkerIfStillCandidate(root_dir_, generation_id_, ep_display_name_, active_logger); + } + + if (!rollback_succeeded) { + if (logger != nullptr && log_recovery_error) { + LogRollbackRecoveryError(ep_display_name_, root_dir_, *logger); + } + return false; + } + + activated_ = false; + return true; + } catch (const std::exception& e) { + if (logger != nullptr) { + logger->Log(LogLevel::Warning, + fmt::format("{}: failed to roll back active bundle marker: {}", ep_display_name_, e.what())); + if (log_recovery_error) { + LogRollbackRecoveryError(ep_display_name_, root_dir_, *logger); + } + } + return false; } } diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h index 9e0379de2..db76d8998 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h @@ -24,6 +24,7 @@ using EpArtifactDownloadFn = std::function* cancel_flag, const std::function& progress_cb, ILogger& logger)>; +using EpBundleManifestFactory = std::function()>; enum class EpBundleInstallPolicy { ReuseVerified, @@ -34,8 +35,8 @@ class EpInstallTransaction { public: EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, std::string ep_display_name, EpBundleManifest manifest, std::string generation_id, - std::filesystem::path bin_dir); - ~EpInstallTransaction(); + std::filesystem::path bin_dir, std::optional previous_active_generation); + ~EpInstallTransaction() noexcept; EpInstallTransaction(const EpInstallTransaction&) = delete; EpInstallTransaction& operator=(const EpInstallTransaction&) = delete; @@ -43,19 +44,26 @@ class EpInstallTransaction { EpInstallTransaction& operator=(EpInstallTransaction&&) = delete; const std::filesystem::path& bin_dir() const { return bin_dir_; } + std::filesystem::path provider_path() const { return bin_dir_ / manifest_.provider_relative_path; } const std::string& bundle_id() const { return manifest_.bundle_id; } - bool CommitActive(ILogger& logger); + bool Activate(ILogger& logger); + void Finalize(ILogger& logger); + bool Rollback(ILogger& logger) noexcept; private: + bool RollbackInternal(ILogger* logger, bool log_recovery_error) noexcept; + std::unique_ptr lock_; std::filesystem::path root_dir_; std::string ep_display_name_; EpBundleManifest manifest_; std::string generation_id_; std::filesystem::path bin_dir_; - bool committed_ = false; + std::optional previous_active_generation_; + bool activated_ = false; + bool finalized_ = false; }; class EpBundleInstaller { diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc index d79379a80..e778d3dd7 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -16,15 +16,30 @@ namespace fl { #ifdef _WIN32 +namespace { + +std::filesystem::path NormalizeDirectory(const std::filesystem::path& directory) { + return std::filesystem::absolute(directory).lexically_normal(); +} + +} // namespace + EpBundleSearchPathOwner::~EpBundleSearchPathOwner() { for (auto it = cookies_.rbegin(); it != cookies_.rend(); ++it) { - RemoveDllDirectory(*it); + if (*it != nullptr) { + RemoveDllDirectory(*it); + } } } +bool EpBundleSearchPathOwner::Owns(const std::filesystem::path& directory) const { + const auto absolute_directory = NormalizeDirectory(directory); + return std::find(directories_.begin(), directories_.end(), absolute_directory) != directories_.end(); +} + bool EpBundleSearchPathOwner::Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger) { - const auto absolute_directory = std::filesystem::absolute(directory).lexically_normal(); - if (std::find(directories_.begin(), directories_.end(), absolute_directory) != directories_.end()) { + const auto absolute_directory = NormalizeDirectory(directory); + if (Owns(absolute_directory)) { return true; } @@ -36,9 +51,28 @@ bool EpBundleSearchPathOwner::Add(const std::filesystem::path& directory, std::s } directories_.push_back(absolute_directory); cookies_.push_back(cookie); - cookies_.push_back(cookie); return true; } + +void EpBundleSearchPathOwner::MergeFrom(EpBundleSearchPathOwner&& other) noexcept { + for (size_t i = 0; i < other.directories_.size() && i < other.cookies_.size(); ++i) { + if (other.cookies_[i] == nullptr) { + continue; + } + + if (Owns(other.directories_[i])) { + RemoveDllDirectory(other.cookies_[i]); + } else { + directories_.push_back(std::move(other.directories_[i])); + cookies_.push_back(other.cookies_[i]); + } + + other.cookies_[i] = nullptr; + } + + other.directories_.clear(); + other.cookies_.clear(); +} #endif } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h index 5ddf99d82..c126aba3d 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -21,7 +21,9 @@ class EpBundleSearchPathOwner { EpBundleSearchPathOwner(const EpBundleSearchPathOwner&) = delete; EpBundleSearchPathOwner& operator=(const EpBundleSearchPathOwner&) = delete; + bool Owns(const std::filesystem::path& directory) const; bool Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger); + void MergeFrom(EpBundleSearchPathOwner&& other) noexcept; private: std::vector directories_; diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index 1f224e247..bb1ec7c67 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -103,8 +103,11 @@ std::optional BuildWebGpuManifest() { namespace fl { -WebGpuEpBootstrapper::WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep) - : register_ep_(std::move(register_ep)), installer_(std::filesystem::path(root_dir), kLockFileName, "WebGPU EP") {} +WebGpuEpBootstrapper::WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, + EpBundleManifestFactory manifest_factory, EpArtifactDownloadFn download_fn) + : register_ep_(std::move(register_ep)), + manifest_factory_(manifest_factory ? std::move(manifest_factory) : [] { return BuildWebGpuManifest(); }), + installer_(std::filesystem::path(root_dir), kLockFileName, "WebGPU EP", std::move(download_fn)) {} const std::string& WebGpuEpBootstrapper::Name() const { return name_; } @@ -141,7 +144,9 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac } #ifdef _WIN32 - if (!search_path_owner_.Add(provider_path.parent_path(), "WebGPU EP", logger)) { + EpBundleSearchPathOwner provisional_search_path_owner; + if (!search_path_owner_.Owns(provider_path.parent_path()) && + !provisional_search_path_owner.Add(provider_path.parent_path(), "WebGPU EP", logger)) { return false; } #endif @@ -152,6 +157,10 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return false; } +#ifdef _WIN32 + search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); +#endif + registered_ = true; if (progress_cb) { @@ -163,7 +172,7 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return true; } - auto manifest = BuildWebGpuManifest(); + auto manifest = manifest_factory_(); if (!manifest.has_value()) { logger.Log(LogLevel::Warning, "WebGPU EP: no bundle available for this platform"); return false; @@ -175,24 +184,33 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return false; } - if (!txn->CommitActive(logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: failed to publish active bundle marker"); + if (!txn->Activate(logger)) { + logger.Log(LogLevel::Warning, "WebGPU EP: failed to activate bundle"); return false; } - const auto provider_path = txn->bin_dir() / manifest->provider_relative_path; + const auto provider_path = txn->provider_path(); #ifdef _WIN32 - if (!search_path_owner_.Add(txn->bin_dir(), "WebGPU EP", logger)) { + EpBundleSearchPathOwner provisional_search_path_owner; + if (!search_path_owner_.Owns(txn->bin_dir()) && + !provisional_search_path_owner.Add(txn->bin_dir(), "WebGPU EP", logger)) { + txn->Rollback(logger); return false; } #endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "WebGPU EP: ORT registration failed"); + txn->Rollback(logger); return false; } +#ifdef _WIN32 + search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); +#endif + registered_ = true; + txn->Finalize(logger); if (progress_cb) { progress_cb(name_, 100.0f); diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index 7bc0ec613..9049733d7 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -20,7 +20,9 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { public: /// @param root_dir Root directory for the WebGPU EP bundle, e.g. "/ep/webgpu-ep". /// @param register_ep Callback to register the EP DLL with ORT. - WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep); + WebGpuEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, + EpBundleManifestFactory manifest_factory = nullptr, + EpArtifactDownloadFn download_fn = nullptr); ~WebGpuEpBootstrapper() override = default; // Non-copyable @@ -39,6 +41,7 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { bool registered_ = false; int attempts_ = 0; EpRegistrationCallback register_ep_; + EpBundleManifestFactory manifest_factory_; EpBundleInstaller installer_; #ifdef _WIN32 EpBundleSearchPathOwner search_path_owner_; diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index 59dec1f56..273a2f310 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -3,21 +3,28 @@ #include "ep_detection/cuda_ep_bootstrapper.h" #include "ep_detection/cuda_ep_manifest.h" +#include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_utils.h" #include "logger.h" +#include "util/sha256.h" #include "utils/scoped_environment_variable.h" #include "utils/temp_path.h" #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include +#include #include namespace fl { @@ -59,6 +66,175 @@ void ExpectUniqueInstalledPaths(const EpBundleManifest& manifest) { } } +class NullLogger : public ILogger { + public: + void Log(LogLevel /*level*/, std::string_view /*message*/) override {} +}; + +std::vector AsBytes(std::string_view text) { return std::vector(text.begin(), text.end()); } + +std::string HashOf(const std::vector& bytes) { + auto tmp = test::TempPath::CreateTempFile("fl_cuda_bootstrapper_hash_"); + std::ofstream out(tmp.path(), std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + return Sha256File(tmp.path()); +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +class FakeDownloads { + public: + void SetSequence(const std::string& url, std::vector> payloads) { + payloads_[url] = std::move(payloads); + } + + EpArtifactDownloadFn AsFn() { + return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, + std::atomic* cancel_flag, const std::function& progress_cb, + ILogger& /*logger*/) -> bool { + auto it = payloads_.find(url); + if (it == payloads_.end() || it->second.empty()) { + return false; + } + + if (progress_cb) { + progress_cb(0.0f); + } + + if (cancel_flag && cancel_flag->load()) { + return false; + } + + int& count = call_counts_[url]; + const size_t index = std::min(static_cast(count), it->second.size() - 1); + const auto& bytes = it->second[index]; + count++; + + std::filesystem::create_directories(destination.parent_path()); + std::ofstream out(destination, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + + if (progress_cb) { + progress_cb(100.0f); + } + + return true; + }; + } + + private: + std::map>> payloads_; + std::map call_counts_; +}; + +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +constexpr const char* kLockFileName = "cuda-ep.lock"; +#if defined(_WIN32) +constexpr const char* kProviderRelativePath = "onnxruntime_providers_cuda.dll"; +#else +constexpr const char* kProviderRelativePath = "libonnxruntime_providers_cuda.so"; +#endif +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +constexpr const char* kGenAiCudaLibrary = "libonnxruntime-genai-cuda.so"; +#endif + +EpBundleManifest MakeRawManifest(FakeDownloads& downloads, const std::string& bundle_id) { + const auto provider_url = std::string("https://example.test/") + bundle_id + "/provider"; + const auto provider_payload = AsBytes(bundle_id + "-provider"); + + downloads.SetSequence(provider_url, {provider_payload}); + + EpBundleManifest manifest; + manifest.bundle_id = bundle_id; + manifest.provider_relative_path = kProviderRelativePath; + manifest.artifacts = {EpBundleArtifact{.id = "provider", + .url = provider_url, + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .ignored_archive_paths = {}, + .archive_max_bytes = 0, + .raw_relative_path = kProviderRelativePath, + .raw_sha256 = HashOf(provider_payload), + .raw_max_bytes = 1024}}; + +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + const auto genai_url = std::string("https://example.test/") + bundle_id + "/genai"; + const auto genai_payload = AsBytes(bundle_id + "-genai"); + + downloads.SetSequence(genai_url, {genai_payload}); + + manifest.artifacts.push_back(EpBundleArtifact{.id = "genai", + .url = genai_url, + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .ignored_archive_paths = {}, + .archive_max_bytes = 0, + .raw_relative_path = kGenAiCudaLibrary, + .raw_sha256 = HashOf(genai_payload), + .raw_max_bytes = 1024}); +#endif + + return manifest; +} + +std::optional InstallAndFinalize(const std::filesystem::path& root, + const EpBundleManifest& manifest, + EpArtifactDownloadFn download_fn, + ILogger& logger) { + EpBundleInstaller installer(root, kLockFileName, "CUDA EP", std::move(download_fn)); + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + if (!txn) { + return std::nullopt; + } + + const auto bin_dir = txn->bin_dir(); + if (!txn->Activate(logger)) { + return std::nullopt; + } + + txn->Finalize(logger); + return bin_dir; +} + +std::optional FindGenerationWithPrefix(const std::filesystem::path& bundles_dir, std::string_view prefix) { + for (const auto& entry : std::filesystem::directory_iterator(bundles_dir)) { + const auto generation = entry.path().filename().string(); + if (generation.starts_with(prefix)) { + return generation; + } + } + + return std::nullopt; +} + +template +CudaEpBootstrapper MakeInstalledBundleBootstrapper(std::string root_dir, RegisterEp register_ep, + const EpBundleManifest& manifest, EpArtifactDownloadFn download_fn) { +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) + auto loader = [](const std::filesystem::path&, ILogger&) -> std::shared_ptr { + return std::shared_ptr(new int(0), [](void* token) { + delete static_cast(token); + }); + }; + + return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), + [manifest] { return std::optional(manifest); }, std::move(download_fn), + loader); +#else + return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), + [manifest] { return std::optional(manifest); }, std::move(download_fn)); +#endif +} +#endif + } // namespace TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { @@ -70,23 +246,23 @@ TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { #endif } -TEST(CudaEpManifestTest, WindowsX64MetadataMatches20260805Bundle) { +TEST(CudaEpManifestTest, WindowsX64MetadataMatches20260806Bundle) { const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsX64); ASSERT_TRUE(manifest.has_value()); - EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260805-050438"); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-x64-cuda-12.8.4-ort-1.28.0-genai-0.15.2-20260806-182620"); EXPECT_EQ(manifest->provider_relative_path, "onnxruntime_providers_cuda.dll"); ASSERT_EQ(manifest->artifacts.size(), 3u); - ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-x64-20260805-050438.zip", - "b47716cbd9a1c92722a6bc914ca57b0e8efea15f7b1a46eecfb2637cadc1bee5", 640 * kMiB, + ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-x64-20260806-182620.zip", + "a90223e4091cfa63b1e40af27a5d5f0267fdfdd15f0459c2922106afe352d306", 640 * kMiB, { {"cublas64_12.dll", "9513540e4ec4c51ee9e7304138c2cc255c29a8c181f9e80c38efa25738becd99"}, {"cublasLt64_12.dll", "b199d1ff892a81b7fd3d57ba1781549609b41500b36008fef326038393ad46c7"}, {"cudart64_12.dll", "c2c9a9c22a9bcba90e261825968836787b331038047a26770cffb7a583c28344"}, }); ExpectArtifact( - manifest->artifacts[1], "cudnn", "cudnn-bins-win-x64-20260805-050438.zip", - "1b065e115c2ac35040053ebe594a8c089906f8cbe5b8d8ed832ba5eb27cdeb5e", 704 * kMiB, + manifest->artifacts[1], "cudnn", "cudnn-bins-win-x64-20260806-182620.zip", + "b82cd271c8c9cbd52ea9e4dedaa4cc3864bf8d7b221d87d5bde81d6ee4a399da", 704 * kMiB, { {"cudnn64_9.dll", "0d1d71325eb5e91570ab8ba8e399e07bf717ffd76511b2407229a8f45e0b1305"}, {"cudnn_adv64_9.dll", "6d66bce22502c2582a9c0e5398ee8cc38addce2c837eb6db8786abc650e48dd8"}, @@ -98,32 +274,32 @@ TEST(CudaEpManifestTest, WindowsX64MetadataMatches20260805Bundle) { {"cudnn_ops64_9.dll", "49487537744256a3d4365c4792b03bf31130ad1faea0a13eafa219620941d837"}, }); ExpectArtifact( - manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-x64-20260805-050438.zip", - "65044a715a2d4b74e77f019988f77c936f5b62973c27cf2d59704cf39057e567", 256 * kMiB, + manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-x64-20260806-182620.zip", + "e62938987e848a0fbb3d215dfefaed40307d2446393909927ba0345eaaf3d263", 256 * kMiB, { - {"onnxruntime-genai-cuda.dll", "612ad6cf3d099431af886537080223a62522e58caba0c7d278b9b4b1eb03c4ce"}, - {"onnxruntime_providers_cuda.dll", "971c1002ce7c16338273f693316bec4862ac74c7efa2ffbd644630cfe10d6e37"}, + {"onnxruntime-genai-cuda.dll", "7894fb5efaad4a663e834f20b912b44cc383629b24ffe8bbc6382786a7326dbc"}, + {"onnxruntime_providers_cuda.dll", "60f1aeef7ebe27f7e659cb88f597005ca5a5e75832b85dcef3eef02b9322df9a"}, }); ExpectUniqueInstalledPaths(*manifest); } -TEST(CudaEpManifestTest, WindowsArm64MetadataMatches20260805Bundle) { +TEST(CudaEpManifestTest, WindowsArm64MetadataMatches20260806Bundle) { const auto manifest = BuildCudaEpManifest(CudaEpPlatform::WindowsArm64); ASSERT_TRUE(manifest.has_value()); - EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260805-050639"); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-win-arm64-cuda-13.4.1-ort-1.28.0-genai-0.15.2-20260806-182803"); EXPECT_EQ(manifest->provider_relative_path, "onnxruntime_providers_cuda.dll"); ASSERT_EQ(manifest->artifacts.size(), 3u); - ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-arm64-20260805-050639.zip", - "b4e0ce6beea87843d02c7d41e04fee3d1a9fb22e0f4fb5e587914cf4f4b94113", 192 * kMiB, + ExpectArtifact(manifest->artifacts[0], "cuda-toolkit", "cuda-bins-win-arm64-20260806-182803.zip", + "de71001db47deb1b59567c50cd5fb1c7705945a9461c95505e987fb8731d6175", 192 * kMiB, { {"cublas64_13.dll", "80b322ce3fe77d1c6c0348e30a31c5f2682da4197680177a179af69275b57997"}, {"cublasLt64_13.dll", "d13048a5f17deeb1a051189c0d5ac898cdf398c6dfca62d100c6eb39329a1d80"}, {"cudart64_13.dll", "32504bd5f424a4e73d3bb5ecc69f018538ae371efa0210bd33e88c7c78b9dca7"}, }); ExpectArtifact( - manifest->artifacts[1], "cudnn", "cudnn-bins-win-arm64-20260805-050639.zip", - "24347fc6b596ae28c32659c82da688bc386da36228e65329df031c028d8527ad", 192 * kMiB, + manifest->artifacts[1], "cudnn", "cudnn-bins-win-arm64-20260806-182803.zip", + "84338552f83a602e989e2a964ed37c342560486031c955225d265402ccf02bd1", 192 * kMiB, { {"cudnn64_9.dll", "247cecbb33132c829c6ed328b7dd34d077a27d0f0fb0ee0b56469ec6bdfd1c17"}, {"cudnn_adv64_9.dll", "b624590960a3ce3ac7c3a5fc683912dbd9ba9de20fa1af52db4485c435c78375"}, @@ -135,28 +311,28 @@ TEST(CudaEpManifestTest, WindowsArm64MetadataMatches20260805Bundle) { {"cudnn_ops64_9.dll", "c9e0ec0e0a4e659393e15897ed1f6e5bac677e0c0fe7e12290f0386f19477b6b"}, }); ExpectArtifact( - manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-arm64-20260805-050639.zip", - "8152d03a0fbef39bd11f5b07dbc6776abd125dbee1dc1d2877a04dc62bbde641", 96 * kMiB, + manifest->artifacts[2], "cuda-ep", "cuda-ep-bins-win-arm64-20260806-182803.zip", + "212e670c61b3292d4a7d98f16fc2cf61f7b080604e0c145e81c39ec81e7b3259", 96 * kMiB, { - {"onnxruntime-genai-cuda.dll", "5284fdec9d4e9e25d6b4cf129205f0c88d3c2f5e678907b2bc1581b575266016"}, - {"onnxruntime_providers_cuda.dll", "b60cd5a26bc180229c9da0dc635d6b3404c306246708291dfae7c9f72ad5e862"}, + {"onnxruntime-genai-cuda.dll", "ab61145f4bc6284286e663586f634b973072d58ced20c497c7e5259f2ef3fc08"}, + {"onnxruntime_providers_cuda.dll", "d92ffbd23a84f91b976baed9031de267efe1dc892d85c09d0979d25b89f5d1a0"}, }); ExpectUniqueInstalledPaths(*manifest); } -TEST(CudaEpManifestTest, LinuxX64MetadataMatches20260805Bundle) { +TEST(CudaEpManifestTest, LinuxX64MetadataMatches20260806Bundle) { const auto manifest = BuildCudaEpManifest(CudaEpPlatform::LinuxX64); ASSERT_TRUE(manifest.has_value()); - EXPECT_EQ(manifest->bundle_id, "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260805-050706"); + EXPECT_EQ(manifest->bundle_id, "cuda-ep-linux-x64-ort-1.28.0-genai-0.15.2-20260806-182830"); EXPECT_EQ(manifest->provider_relative_path, "libonnxruntime_providers_cuda.so"); ASSERT_EQ(manifest->artifacts.size(), 1u); ExpectArtifact( - manifest->artifacts[0], "cuda-ep", "cuda-ep-linux-x64-20260805-050706.zip", - "2bc3e5949b75d7521d903c958716c06602ddaa5c2a1f98bd12811294db738c37", 448 * kMiB, + manifest->artifacts[0], "cuda-ep", "cuda-ep-linux-x64-20260806-182830.zip", + "abf347e7234d7434105efde12a2e0609fdd1d8828167b9873f4463926f1206e6", 448 * kMiB, { - {"libonnxruntime-genai-cuda.so", "8b26db7a085de61653ebaaa8fc221b720879fe74583eb01204f11bf22638c345"}, - {"libonnxruntime_providers_cuda.so", "da94d951b89dc84c44b10f7faf52b17e675b3f1a13d8f32808264d425d0464bd"}, + {"libonnxruntime-genai-cuda.so", "d5300fc4413d9e74bd8dfceb5233fca6fcfa1d5ddc247081365fdb5f143091e6"}, + {"libonnxruntime_providers_cuda.so", "b88d7b7f4b2e81d3eff41663fc70f4ae9e03dee9e2301cb53dc250e5a96d7f7a"}, }); ExpectUniqueInstalledPaths(*manifest); } @@ -185,4 +361,200 @@ TEST(CudaEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) EXPECT_EQ(registration_count, 0); } +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ + (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +TEST(CudaEpBootstrapperTest, BundleActivationFailurePreventsRegistration) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + FakeDownloads downloads; + const auto manifest = MakeRawManifest(downloads, "bundle-v1"); + + std::filesystem::create_directories(root.path()); + std::filesystem::create_directory(root.path() / "active"); + { + std::ofstream(root.path() / "active" / "blocker") << "x"; + } + + int registration_count = 0; + auto bootstrapper = MakeInstalledBundleBootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return true; + }, + manifest, downloads.AsFn()); + NullLogger logger; + + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 0); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_TRUE(std::filesystem::is_directory(root.path() / "active")); +} + +TEST(CudaEpBootstrapperTest, BundleRegistrationFailureRollsBackToPreviousGeneration) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + FakeDownloads downloads; + const auto manifest_v1 = MakeRawManifest(downloads, "bundle-v1"); + const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); + NullLogger logger; + + ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + int registration_count = 0; + auto bootstrapper = MakeInstalledBundleBootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return false; + }, + manifest_v2, downloads.AsFn()); + + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 1); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + + const auto candidate_v2 = FindGenerationWithPrefix(root.path() / "bundles", "bundle-v2-"); + ASSERT_TRUE(candidate_v2.has_value()); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / *candidate_v2)); +} + +TEST(CudaEpBootstrapperTest, SuccessfulBundleRegistrationFinalizesPreviousGenerationCleanup) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + FakeDownloads downloads; + const auto manifest_v1 = MakeRawManifest(downloads, "bundle-v1"); + const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); + NullLogger logger; + + ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + int registration_count = 0; + std::filesystem::path registered_path; + auto bootstrapper = MakeInstalledBundleBootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path& path) { + ++registration_count; + registered_path = path; + return true; + }, + manifest_v2, downloads.AsFn()); + + EXPECT_TRUE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 1); + EXPECT_TRUE(bootstrapper.IsRegistered()); + EXPECT_EQ(registered_path.filename().string(), kProviderRelativePath); + + const auto active_v2 = ReadFile(root.path() / "active"); + EXPECT_TRUE(active_v2.starts_with("bundle-v2-")); + EXPECT_FALSE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); +} +#endif + +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +TEST(CudaEpBootstrapperTest, InstalledBundleGenAiDependencyLoaderFailureRollsBackMarkerAndSkipsRegistration) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + FakeDownloads downloads; + const auto manifest_v1 = MakeRawManifest(downloads, "bundle-v1"); + const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); + NullLogger logger; + + ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + int registration_count = 0; + std::vector requested_paths; + auto loader = [&](const std::filesystem::path& path, ILogger&) -> std::shared_ptr { + requested_paths.push_back(path); + return {}; + }; + + CudaEpBootstrapper bootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return true; + }, + [manifest_v2] { return std::optional(manifest_v2); }, downloads.AsFn(), loader); + + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 0); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + + const auto candidate_v2 = FindGenerationWithPrefix(root.path() / "bundles", "bundle-v2-"); + ASSERT_TRUE(candidate_v2.has_value()); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / *candidate_v2)); + + ASSERT_EQ(requested_paths.size(), 1u); + EXPECT_EQ(requested_paths[0], std::filesystem::absolute(root.path() / "bundles" / *candidate_v2 / "bin" / + kGenAiCudaLibrary) + .lexically_normal()); +} + +TEST(CudaEpBootstrapperTest, OverrideRegistrationFailureDoesNotPoisonGenAiCudaRetryState) { + auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); + const auto first_dir = root.path() / "first"; + const auto second_dir = root.path() / "second"; + std::filesystem::create_directories(first_dir); + std::filesystem::create_directories(second_dir); + const auto first_provider_path = first_dir / "custom_cuda_provider"; + const auto second_provider_path = second_dir / "custom_cuda_provider"; + std::ofstream(first_provider_path, std::ios::binary) << "test provider"; + std::ofstream(second_provider_path, std::ios::binary) << "test provider"; + + int registration_count = 0; + int load_count = 0; + int release_count = 0; + bool allow_registration = false; + std::vector requested_paths; + auto loader = [&](const std::filesystem::path& path, ILogger&) -> std::shared_ptr { + requested_paths.push_back(path); + ++load_count; + return std::shared_ptr(new int(load_count), [&](void* token) { + ++release_count; + delete static_cast(token); + }); + }; + + CudaEpBootstrapper bootstrapper(root.string(), [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return allow_registration; + }, + /*manifest_factory=*/nullptr, + /*download_fn=*/nullptr, loader); + StderrLogger logger; + + { + test::ScopedEnvironmentVariable override(kOverrideEnv, first_provider_path.string()); + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + } + + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(registration_count, 1); + EXPECT_EQ(load_count, 1); + EXPECT_EQ(release_count, 1); + ASSERT_EQ(requested_paths.size(), 1u); + EXPECT_EQ(requested_paths[0], + std::filesystem::absolute(first_dir / "libonnxruntime-genai-cuda.so").lexically_normal()); + + allow_registration = true; + { + test::ScopedEnvironmentVariable override(kOverrideEnv, second_provider_path.string()); + EXPECT_TRUE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + } + + EXPECT_TRUE(bootstrapper.IsRegistered()); + EXPECT_EQ(registration_count, 2); + EXPECT_EQ(load_count, 2) << "registration failure must not keep a provisional preload alive across retries"; + EXPECT_EQ(release_count, 1) << "the successful retry keeps its preload owned by the bootstrapper"; + ASSERT_EQ(requested_paths.size(), 2u); + EXPECT_EQ(requested_paths[1], + std::filesystem::absolute(second_dir / "libonnxruntime-genai-cuda.so").lexically_normal()); +} +#endif + } // namespace fl diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc index 90fb971f7..68f5f976c 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -11,8 +11,10 @@ #include +#include #include #include +#include #include #include #include @@ -29,6 +31,21 @@ class NullLogger : public ILogger { void Log(LogLevel /*level*/, std::string_view /*message*/) override {} }; +class RecordingLogger : public ILogger { + public: + void Log(LogLevel level, std::string_view message) override { + entries.emplace_back(level, std::string(message)); + } + + bool Contains(std::string_view fragment) const { + return std::any_of(entries.begin(), entries.end(), [&](const auto& entry) { + return entry.second.find(fragment) != std::string::npos; + }); + } + + std::vector> entries; +}; + std::vector AsBytes(const std::string& text) { return std::vector(text.begin(), text.end()); } std::string HashOf(const std::vector& bytes) { @@ -129,18 +146,19 @@ EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::st return manifest; } -std::optional InstallAndCommit(EpBundleInstaller& installer, const EpBundleManifest& manifest, - ILogger& logger) { +std::optional InstallAndFinalize(EpBundleInstaller& installer, const EpBundleManifest& manifest, + ILogger& logger) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); if (!txn) { return std::nullopt; } const auto bin_dir = txn->bin_dir(); - if (!txn->CommitActive(logger)) { + if (!txn->Activate(logger)) { return std::nullopt; } + txn->Finalize(logger); return bin_dir; } @@ -190,7 +208,8 @@ TEST(EpBundleInstallerTest, ReusesValidBundleWithoutRedownloading) { auto first = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(first, nullptr); first_bin = first->bin_dir(); - ASSERT_TRUE(first->CommitActive(logger)); + ASSERT_TRUE(first->Activate(logger)); + first->Finalize(logger); } auto second = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); @@ -236,7 +255,7 @@ TEST(EpBundleInstallerTest, ForceDownloadRedownloadsEveryArtifactFromValidActive EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); auto replacement = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger, EpBundleInstallPolicy::ForceDownload); @@ -349,7 +368,7 @@ TEST(EpBundleInstallerTest, MissingExpectedExtractedFileFails) { EXPECT_EQ(txn, nullptr); } -TEST(EpBundleInstallerTest, CommitActiveWritesActiveMarkerFile) { +TEST(EpBundleInstallerTest, ActivateWritesActiveMarkerFile) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload = AsBytes("content"); FakeDownloads downloads; @@ -361,12 +380,12 @@ TEST(EpBundleInstallerTest, CommitActiveWritesActiveMarkerFile) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - EXPECT_TRUE(txn->CommitActive(logger)); + EXPECT_TRUE(txn->Activate(logger)); EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-42-")); } -TEST(EpBundleInstallerTest, CommitActiveReplacesExistingActiveMarker) { +TEST(EpBundleInstallerTest, ActivateReplacesExistingActiveMarker) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload = AsBytes("content"); FakeDownloads downloads; @@ -382,7 +401,7 @@ TEST(EpBundleInstallerTest, CommitActiveReplacesExistingActiveMarker) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - EXPECT_TRUE(txn->CommitActive(logger)); + EXPECT_TRUE(txn->Activate(logger)); EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-1-")) << "publishing replaces an existing active marker atomically"; } @@ -407,7 +426,7 @@ TEST(EpBundleInstallerTest, InstallTransactionHoldsLockUntilReleased) { EXPECT_NO_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }); } -TEST(EpBundleInstallerTest, CommitActiveRevalidatesUnderLockAndRefusesTamperedBundle) { +TEST(EpBundleInstallerTest, ActivateRevalidatesUnderLockAndRefusesTamperedBundle) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload_v1 = AsBytes("v1"); auto payload_v2 = AsBytes("v2"); @@ -418,7 +437,7 @@ TEST(EpBundleInstallerTest, CommitActiveRevalidatesUnderLockAndRefusesTamperedBu NullLogger logger; auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); - ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); const auto active_v1 = ReadFile(root.path() / "active"); ASSERT_TRUE(active_v1.starts_with("bundle-v1-")); @@ -429,14 +448,14 @@ TEST(EpBundleInstallerTest, CommitActiveRevalidatesUnderLockAndRefusesTamperedBu std::ofstream(txn->bin_dir() / "unexpected.txt") << "surprise"; } - EXPECT_FALSE(txn->CommitActive(logger)) << "re-verification under the lock must reject a tampered bundle"; + EXPECT_FALSE(txn->Activate(logger)) << "re-verification under the lock must reject a tampered bundle"; EXPECT_EQ(ReadFile(root.path() / "active"), active_v1) << "a bundle failing re-verification must not advance the active marker"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) << "the previous generation is preserved when activation fails"; } -TEST(EpBundleInstallerTest, CommitActivePreservesPreviousMarkerWhenPublicationFails) { +TEST(EpBundleInstallerTest, ActivatePreservesPreviousMarkerWhenPublicationFails) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload_v1 = AsBytes("v1"); auto payload_v2 = AsBytes("v2"); @@ -447,7 +466,7 @@ TEST(EpBundleInstallerTest, CommitActivePreservesPreviousMarkerWhenPublicationFa NullLogger logger; auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); - ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); const auto active_v1 = ReadFile(root.path() / "active"); ASSERT_TRUE(active_v1.starts_with("bundle-v1-")); @@ -461,7 +480,7 @@ TEST(EpBundleInstallerTest, CommitActivePreservesPreviousMarkerWhenPublicationFa std::ofstream(root.path() / "active" / "blocker") << "x"; } - EXPECT_FALSE(txn->CommitActive(logger)) << "a failed marker publication is reported as failure"; + EXPECT_FALSE(txn->Activate(logger)) << "a failed marker publication is reported as failure"; EXPECT_TRUE(std::filesystem::is_directory(root.path() / "active")) << "the failed publication left the marker path untouched"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) @@ -469,7 +488,7 @@ TEST(EpBundleInstallerTest, CommitActivePreservesPreviousMarkerWhenPublicationFa EXPECT_TRUE(std::filesystem::exists(txn->bin_dir())); } -TEST(EpBundleInstallerTest, CommitActiveRemovesOldGenerations) { +TEST(EpBundleInstallerTest, FinalizeRemovesOldGenerationsOnlyAfterSuccessfulActivation) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload_v1 = AsBytes("v1"); auto payload_v2 = AsBytes("v2"); @@ -481,19 +500,211 @@ TEST(EpBundleInstallerTest, CommitActiveRemovesOldGenerations) { NullLogger logger; auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); - ASSERT_TRUE(InstallAndCommit(installer, manifest_v1, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); const auto active_v1 = ReadFile(root.path() / "active"); ASSERT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); - ASSERT_TRUE(InstallAndCommit(installer, manifest_v2, logger).has_value()); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); const auto active_v2 = ReadFile(root.path() / "active"); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) + << "activation must not delete the previously active generation"; + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); + + txn->Finalize(logger); + EXPECT_FALSE(std::filesystem::exists(root.path() / "bundles" / active_v1)) - << "the previous generation is removed once the new one is committed"; + << "finalization removes the previous generation only after registration succeeds"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); } +TEST(EpBundleInstallerTest, RollbackRestoresPreviousMarkerAndRetainsGenerations) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + ASSERT_NE(ReadFile(root.path() / "active"), active_v1); + + const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); + EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); +} + +TEST(EpBundleInstallerTest, RollbackFirstInstallRemovesCandidateMarkerAndRetainsGeneration) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("v1"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload)); + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + + const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); + ASSERT_EQ(ReadFile(root.path() / "active"), candidate_generation); + + EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_FALSE(std::filesystem::exists(root.path() / "active")); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); +} + +TEST(EpBundleInstallerTest, RollbackFirstInstallLeavesDifferentMarkerUntouched) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("v1"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload)); + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + + { + std::ofstream(root.path() / "active", std::ios::binary | std::ios::trunc) << "manual-marker"; + } + + EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_EQ(ReadFile(root.path() / "active"), "manual-marker"); +} + +TEST(EpBundleInstallerTest, RollbackOnReusedActiveBundleLeavesMarkerUnchanged) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload = AsBytes("v1"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload)); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); + const auto active_generation = ReadFile(root.path() / "active"); + + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + ASSERT_EQ(ReadFile(root.path() / "active"), active_generation); + + EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_EQ(ReadFile(root.path() / "active"), active_generation); +} + +TEST(EpBundleInstallerTest, ActivatedTransactionDestructorRestoresPreviousMarkerBeforeReleasingLock) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + NullLogger logger; + + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + + const auto lock_path = root.path() / "test.lock"; + EXPECT_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }, std::runtime_error); + + txn.reset(); + + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); + EXPECT_NO_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }); +} + +TEST(EpBundleInstallerTest, RollbackFailureLogsCacheRecoveryMessageAndRetainsGenerations) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + RecordingLogger logger; + + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + + const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); + std::filesystem::remove(root.path() / "active"); + std::filesystem::create_directory(root.path() / "active"); + { + std::ofstream(root.path() / "active" / "blocker") << "x"; + } + + EXPECT_FALSE(txn->Rollback(logger)); + EXPECT_TRUE(logger.Contains("failed to recover the active bundle marker after bootstrap failure")); + EXPECT_TRUE(logger.Contains(root.path().string())); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); +} + +TEST(EpBundleInstallerTest, FinalizeCleanupFailureKeepsPublishedMarker) { + auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); + auto payload_v1 = AsBytes("v1"); + auto payload_v2 = AsBytes("v2"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/v1.so", {payload_v1}); + downloads.SetSequence("https://example.test/v2.so", {payload_v2}); + EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); + RecordingLogger logger; + + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload_v1)); + ASSERT_TRUE(InstallAndFinalize(installer, manifest_v1, logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); + auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); + ASSERT_NE(txn, nullptr); + ASSERT_TRUE(txn->Activate(logger)); + const auto active_v2 = ReadFile(root.path() / "active"); + + std::filesystem::rename(root.path() / "bundles", root.path() / "bundles_saved"); + { + std::ofstream(root.path() / "bundles") << "block cleanup"; + } + + txn->Finalize(logger); + txn.reset(); + + EXPECT_EQ(ReadFile(root.path() / "active"), active_v2); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles_saved" / active_v1)); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles_saved" / active_v2)); +} + TEST(EpBundleInstallerTest, StaleStagingDirectoryIsCleanedUpOnNextInstall) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); std::filesystem::create_directories(root.path() / "staging" / "leftover-from-a-crash"); @@ -580,7 +791,7 @@ TEST(EpBundleInstallerTest, DoesNotCopyUnexpectedFilesFromExistingBundle) { EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); auto bin_dir = root.path() / "bundles" / ReadFile(root.path() / "active") / "bin"; { @@ -729,7 +940,7 @@ TEST(EpBundleInstallerTest, ReuseDependsOnlyOnInstalledRuntimeFiles) { EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); manifest.artifacts.front().ignored_archive_paths = {"different-packaging-metadata.json"}; auto reused = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); @@ -776,7 +987,7 @@ TEST(EpBundleInstallerTest, ReusesValidArtifactsAndDownloadsOnlyMismatches) { EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); const auto active_bin = root.path() / "bundles" / ReadFile(root.path() / "active") / "bin"; std::ofstream(active_bin / "second.bin", std::ios::binary | std::ios::trunc) << "corrupt"; @@ -848,7 +1059,7 @@ TEST(EpBundleInstallerTest, CancellationOnVerifiedBundleReuseReturnsNoTransactio const auto manifest = MakeRawManifest("bundle-1", "https://example.test/provider.so", HashOf(payload)); EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); const auto active_generation = ReadFile(root.path() / "active"); auto cancel_reuse = [](const std::string&, float percent) { return percent != kEpReadyToRegisterProgress; }; @@ -895,7 +1106,7 @@ TEST(EpBundleInstallerTest, CancellationAfterCopiedArtifactRemovesStagingCopy) { EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); NullLogger logger; - ASSERT_TRUE(InstallAndCommit(installer, manifest, logger).has_value()); + ASSERT_TRUE(InstallAndFinalize(installer, manifest, logger).has_value()); const auto active_generation = ReadFile(root.path() / "active"); const auto active_bin = root.path() / "bundles" / active_generation / "bin"; std::ofstream(active_bin / "second.bin", std::ios::binary | std::ios::trunc) << "corrupt"; diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc index f83d3dca9..3c40e75ca 100644 --- a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -2,15 +2,23 @@ // Licensed under the MIT License. #include "ep_detection/webgpu_ep_bootstrapper.h" +#include "ep_detection/ep_bundle_installer.h" #include "logger.h" +#include "util/sha256.h" #include "utils/scoped_environment_variable.h" #include "utils/temp_path.h" #include +#include +#include #include #include +#include +#include +#include #include +#include #include #include @@ -20,6 +28,126 @@ namespace { constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; constexpr const char* kScopedEnvironmentVariableTestEnv = "FOUNDRY_LOCAL_SCOPED_ENVIRONMENT_VARIABLE_TEST"; +constexpr const char* kLockFileName = "webgpu-ep.lock"; + +class RecordingLogger : public ILogger { + public: + void Log(LogLevel level, std::string_view message) override { + entries.emplace_back(level, std::string(message)); + } + + std::vector> entries; +}; + +std::vector AsBytes(std::string_view text) { return std::vector(text.begin(), text.end()); } + +std::string HashOf(const std::vector& bytes) { + auto tmp = test::TempPath::CreateTempFile("fl_webgpu_bootstrapper_hash_"); + std::ofstream out(tmp.path(), std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + return Sha256File(tmp.path()); +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +class FakeDownloads { + public: + void SetSequence(const std::string& url, std::vector> payloads) { + payloads_[url] = std::move(payloads); + } + + EpArtifactDownloadFn AsFn() { + return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, + std::atomic* cancel_flag, const std::function& progress_cb, + ILogger& /*logger*/) -> bool { + auto it = payloads_.find(url); + if (it == payloads_.end() || it->second.empty()) { + return false; + } + + if (progress_cb) { + progress_cb(0.0f); + } + + if (cancel_flag && cancel_flag->load()) { + return false; + } + + int& count = call_counts_[url]; + const size_t index = std::min(static_cast(count), it->second.size() - 1); + const auto& bytes = it->second[index]; + count++; + + std::filesystem::create_directories(destination.parent_path()); + std::ofstream out(destination, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + + if (progress_cb) { + progress_cb(100.0f); + } + + return true; + }; + } + + private: + std::map>> payloads_; + std::map call_counts_; +}; + +EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url) { + const auto payload = AsBytes(bundle_id + "-provider"); + + EpBundleManifest manifest; + manifest.bundle_id = bundle_id; + manifest.provider_relative_path = "provider.so"; + manifest.artifacts = {EpBundleArtifact{.id = "provider", + .url = url, + .is_archive = false, + .archive_sha256 = "", + .extracted_files = {}, + .ignored_archive_paths = {}, + .archive_max_bytes = 0, + .raw_relative_path = "provider.so", + .raw_sha256 = HashOf(payload), + .raw_max_bytes = 1024}}; + return manifest; +} + +std::optional InstallAndFinalize(const std::filesystem::path& root, + const EpBundleManifest& manifest, + EpArtifactDownloadFn download_fn, + ILogger& logger) { + EpBundleInstaller installer(root, kLockFileName, "WebGPU EP", std::move(download_fn)); + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + if (!txn) { + return std::nullopt; + } + + const auto bin_dir = txn->bin_dir(); + if (!txn->Activate(logger)) { + return std::nullopt; + } + + txn->Finalize(logger); + return bin_dir; +} + +std::optional FindGenerationWithPrefix(const std::filesystem::path& bundles_dir, std::string_view prefix) { + for (const auto& entry : std::filesystem::directory_iterator(bundles_dir)) { + const auto generation = entry.path().filename().string(); + if (generation.starts_with(prefix)) { + return generation; + } + } + + return std::nullopt; +} } // namespace @@ -122,4 +250,101 @@ TEST(WebGpuEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFals EXPECT_EQ(registration_count, 0); } +TEST(WebGpuEpBootstrapperTest, BundleActivationFailurePreventsRegistration) { + auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider-v1.so", {AsBytes("bundle-v1-provider")}); + const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/provider-v1.so"); + + std::filesystem::create_directories(root.path()); + std::filesystem::create_directory(root.path() / "active"); + { + std::ofstream(root.path() / "active" / "blocker") << "x"; + } + + int registration_count = 0; + RecordingLogger logger; + WebGpuEpBootstrapper bootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return true; + }, + [manifest] { return std::optional(manifest); }, + downloads.AsFn()); + + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 0); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_TRUE(std::filesystem::is_directory(root.path() / "active")); +} + +TEST(WebGpuEpBootstrapperTest, BundleRegistrationFailureRollsBackToPreviousGeneration) { + auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider-v1.so", {AsBytes("bundle-v1-provider")}); + downloads.SetSequence("https://example.test/provider-v2.so", {AsBytes("bundle-v2-provider")}); + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/provider-v1.so"); + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/provider-v2.so"); + RecordingLogger logger; + + ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + int registration_count = 0; + WebGpuEpBootstrapper bootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path&) { + ++registration_count; + return false; + }, + [manifest_v2] { return std::optional(manifest_v2); }, + downloads.AsFn()); + + EXPECT_FALSE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 1); + EXPECT_FALSE(bootstrapper.IsRegistered()); + EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + + const auto candidate_v2 = FindGenerationWithPrefix(root.path() / "bundles", "bundle-v2-"); + ASSERT_TRUE(candidate_v2.has_value()); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / *candidate_v2)); +} + +TEST(WebGpuEpBootstrapperTest, SuccessfulBundleRegistrationFinalizesPreviousGenerationCleanup) { + auto root = test::TempPath::CreateTempDir("fl_webgpu_bootstrapper_"); + FakeDownloads downloads; + downloads.SetSequence("https://example.test/provider-v1.so", {AsBytes("bundle-v1-provider")}); + downloads.SetSequence("https://example.test/provider-v2.so", {AsBytes("bundle-v2-provider")}); + const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/provider-v1.so"); + const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/provider-v2.so"); + RecordingLogger logger; + + ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + const auto active_v1 = ReadFile(root.path() / "active"); + + int registration_count = 0; + std::filesystem::path registered_path; + WebGpuEpBootstrapper bootstrapper( + root.string(), + [&](const std::string&, const std::filesystem::path& path) { + ++registration_count; + registered_path = path; + return true; + }, + [manifest_v2] { return std::optional(manifest_v2); }, + downloads.AsFn()); + + EXPECT_TRUE(bootstrapper.DownloadAndRegister(false, /*progress_cb=*/nullptr, logger)); + EXPECT_EQ(registration_count, 1); + EXPECT_TRUE(bootstrapper.IsRegistered()); + EXPECT_EQ(registered_path.filename(), "provider.so"); + + const auto active_v2 = ReadFile(root.path() / "active"); + EXPECT_TRUE(active_v2.starts_with("bundle-v2-")); + EXPECT_FALSE(std::filesystem::exists(root.path() / "bundles" / active_v1)); + EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); +} + } // namespace fl From fa7ed6506194277d1b3f34c158917170af291281 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Thu, 6 Aug 2026 20:38:41 +0000 Subject: [PATCH 08/11] test helpers --- .../internal_api/cuda_ep_bootstrapper_test.cc | 116 ++-------------- .../internal_api/ep_bundle_installer_test.cc | 117 ++-------------- .../internal_api/ep_bundle_test_helpers.h | 129 ++++++++++++++++++ .../webgpu_ep_bootstrapper_test.cc | 123 ++--------------- 4 files changed, 165 insertions(+), 320 deletions(-) create mode 100644 sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index 273a2f310..fe1bcfa83 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -5,20 +5,18 @@ #include "ep_detection/cuda_ep_manifest.h" #include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_utils.h" +#include "internal_api/ep_bundle_test_helpers.h" +#include "internal_api/test_helpers.h" #include "logger.h" -#include "util/sha256.h" #include "utils/scoped_environment_variable.h" #include "utils/temp_path.h" #include #include -#include #include #include #include -#include -#include #include #include #include @@ -66,71 +64,12 @@ void ExpectUniqueInstalledPaths(const EpBundleManifest& manifest) { } } -class NullLogger : public ILogger { - public: - void Log(LogLevel /*level*/, std::string_view /*message*/) override {} -}; - -std::vector AsBytes(std::string_view text) { return std::vector(text.begin(), text.end()); } - -std::string HashOf(const std::vector& bytes) { - auto tmp = test::TempPath::CreateTempFile("fl_cuda_bootstrapper_hash_"); - std::ofstream out(tmp.path(), std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - return Sha256File(tmp.path()); -} - -std::string ReadFile(const std::filesystem::path& path) { - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -class FakeDownloads { - public: - void SetSequence(const std::string& url, std::vector> payloads) { - payloads_[url] = std::move(payloads); - } - - EpArtifactDownloadFn AsFn() { - return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, - std::atomic* cancel_flag, const std::function& progress_cb, - ILogger& /*logger*/) -> bool { - auto it = payloads_.find(url); - if (it == payloads_.end() || it->second.empty()) { - return false; - } - - if (progress_cb) { - progress_cb(0.0f); - } - - if (cancel_flag && cancel_flag->load()) { - return false; - } - - int& count = call_counts_[url]; - const size_t index = std::min(static_cast(count), it->second.size() - 1); - const auto& bytes = it->second[index]; - count++; - - std::filesystem::create_directories(destination.parent_path()); - std::ofstream out(destination, std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - - if (progress_cb) { - progress_cb(100.0f); - } - - return true; - }; - } - - private: - std::map>> payloads_; - std::map call_counts_; -}; +using test::AsBytes; +using test::FakeDownloads; +using test::FindGenerationWithPrefix; +using test::HashOf; +using test::NullLogger; +using test::ReadFile; #if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) @@ -185,36 +124,6 @@ EpBundleManifest MakeRawManifest(FakeDownloads& downloads, const std::string& bu return manifest; } -std::optional InstallAndFinalize(const std::filesystem::path& root, - const EpBundleManifest& manifest, - EpArtifactDownloadFn download_fn, - ILogger& logger) { - EpBundleInstaller installer(root, kLockFileName, "CUDA EP", std::move(download_fn)); - auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); - if (!txn) { - return std::nullopt; - } - - const auto bin_dir = txn->bin_dir(); - if (!txn->Activate(logger)) { - return std::nullopt; - } - - txn->Finalize(logger); - return bin_dir; -} - -std::optional FindGenerationWithPrefix(const std::filesystem::path& bundles_dir, std::string_view prefix) { - for (const auto& entry : std::filesystem::directory_iterator(bundles_dir)) { - const auto generation = entry.path().filename().string(); - if (generation.starts_with(prefix)) { - return generation; - } - } - - return std::nullopt; -} - template CudaEpBootstrapper MakeInstalledBundleBootstrapper(std::string root_dir, RegisterEp register_ep, const EpBundleManifest& manifest, EpArtifactDownloadFn download_fn) { @@ -397,7 +306,8 @@ TEST(CudaEpBootstrapperTest, BundleRegistrationFailureRollsBackToPreviousGenerat const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); NullLogger logger; - ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + ASSERT_TRUE(test::InstallAndFinalize(root.path(), kLockFileName, "CUDA EP", manifest_v1, downloads.AsFn(), logger) + .has_value()); const auto active_v1 = ReadFile(root.path() / "active"); int registration_count = 0; @@ -427,7 +337,8 @@ TEST(CudaEpBootstrapperTest, SuccessfulBundleRegistrationFinalizesPreviousGenera const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); NullLogger logger; - ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + ASSERT_TRUE(test::InstallAndFinalize(root.path(), kLockFileName, "CUDA EP", manifest_v1, downloads.AsFn(), logger) + .has_value()); const auto active_v1 = ReadFile(root.path() / "active"); int registration_count = 0; @@ -461,7 +372,8 @@ TEST(CudaEpBootstrapperTest, InstalledBundleGenAiDependencyLoaderFailureRollsBac const auto manifest_v2 = MakeRawManifest(downloads, "bundle-v2"); NullLogger logger; - ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + ASSERT_TRUE(test::InstallAndFinalize(root.path(), kLockFileName, "CUDA EP", manifest_v1, downloads.AsFn(), logger) + .has_value()); const auto active_v1 = ReadFile(root.path() / "active"); int registration_count = 0; diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc index 68f5f976c..217ebccf7 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -2,9 +2,10 @@ // Licensed under the MIT License. #include "ep_detection/ep_bundle_installer.h" +#include "internal_api/ep_bundle_test_helpers.h" +#include "internal_api/test_helpers.h" #include "logger.h" #include "util/file_lock.h" -#include "util/sha256.h" #include "utils/temp_path.h" #include "utils/zip_builder.h" @@ -12,10 +13,7 @@ #include #include -#include #include -#include -#include #include #include #include @@ -26,11 +24,6 @@ namespace fl { namespace { -class NullLogger : public ILogger { - public: - void Log(LogLevel /*level*/, std::string_view /*message*/) override {} -}; - class RecordingLogger : public ILogger { public: void Log(LogLevel level, std::string_view message) override { @@ -46,69 +39,12 @@ class RecordingLogger : public ILogger { std::vector> entries; }; -std::vector AsBytes(const std::string& text) { return std::vector(text.begin(), text.end()); } - -std::string HashOf(const std::vector& bytes) { - auto tmp = test::TempPath::CreateTempFile("fl_bundle_installer_hash_"); - std::ofstream out(tmp.path(), std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - return Sha256File(tmp.path()); -} - -std::string ReadFile(const std::filesystem::path& path) { - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -class FakeDownloads { - public: - void SetSequence(const std::string& url, std::vector> payloads) { - payloads_[url] = std::move(payloads); - } - - int CallCount(const std::string& url) const { - auto it = call_counts_.find(url); - return it == call_counts_.end() ? 0 : it->second; - } - - EpArtifactDownloadFn AsFn() { - return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, - std::atomic* cancel_flag, const std::function& progress_cb, - ILogger& /*logger*/) -> bool { - auto it = payloads_.find(url); - if (it == payloads_.end() || it->second.empty()) { - return false; - } - - if (progress_cb) { - progress_cb(0.0f); - } - if (cancel_flag && cancel_flag->load()) { - return false; - } - - int& count = call_counts_[url]; - size_t index = std::min(static_cast(count), it->second.size() - 1); - const auto& bytes = it->second[index]; - count++; - - std::filesystem::create_directories(destination.parent_path()); - std::ofstream out(destination, std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - - if (progress_cb) { - progress_cb(100.0f); - } - return true; - }; - } - - private: - std::map>> payloads_; - std::map call_counts_; -}; +using test::AsBytes; +using test::FakeDownloads; +using test::HashOf; +using test::InstallAndFinalize; +using test::NullLogger; +using test::ReadFile; EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url, const std::string& sha256) { EpBundleManifest manifest; @@ -146,22 +82,6 @@ EpBundleManifest MakeArchiveManifest(const std::string& bundle_id, const std::st return manifest; } -std::optional InstallAndFinalize(EpBundleInstaller& installer, const EpBundleManifest& manifest, - ILogger& logger) { - auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); - if (!txn) { - return std::nullopt; - } - - const auto bin_dir = txn->bin_dir(); - if (!txn->Activate(logger)) { - return std::nullopt; - } - - txn->Finalize(logger); - return bin_dir; -} - } // namespace TEST(EpBundleInstallerTest, UnsupportedManifestFailsClosedWithoutDownloading) { @@ -569,27 +489,6 @@ TEST(EpBundleInstallerTest, RollbackFirstInstallRemovesCandidateMarkerAndRetains EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); } -TEST(EpBundleInstallerTest, RollbackFirstInstallLeavesDifferentMarkerUntouched) { - auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); - auto payload = AsBytes("v1"); - FakeDownloads downloads; - downloads.SetSequence("https://example.test/v1.so", {payload}); - EpBundleInstaller installer(root.path(), "test.lock", "TestEP", downloads.AsFn()); - NullLogger logger; - - const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload)); - auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); - ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); - - { - std::ofstream(root.path() / "active", std::ios::binary | std::ios::trunc) << "manual-marker"; - } - - EXPECT_TRUE(txn->Rollback(logger)); - EXPECT_EQ(ReadFile(root.path() / "active"), "manual-marker"); -} - TEST(EpBundleInstallerTest, RollbackOnReusedActiveBundleLeavesMarkerUnchanged) { auto root = test::TempPath::CreateTempDir("fl_bundle_installer_"); auto payload = AsBytes("v1"); diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h b/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h new file mode 100644 index 000000000..eb312c01c --- /dev/null +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include "ep_detection/ep_bundle_installer.h" +#include "util/sha256.h" +#include "utils/temp_path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fl::test { + +inline std::vector AsBytes(std::string_view text) { + return std::vector(text.begin(), text.end()); +} + +inline std::string HashOf(const std::vector& bytes) { + auto tmp = TempPath::CreateTempFile("fl_ep_bundle_hash_"); + std::ofstream out(tmp.path(), std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + return Sha256File(tmp.path()); +} + +inline std::string ReadFile(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +class FakeDownloads { + public: + void SetSequence(const std::string& url, std::vector> payloads) { + payloads_[url] = std::move(payloads); + } + + int CallCount(const std::string& url) const { + const auto it = call_counts_.find(url); + return it == call_counts_.end() ? 0 : it->second; + } + + EpArtifactDownloadFn AsFn() { + return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, + std::atomic* cancel_flag, const std::function& progress_cb, + ILogger& /*logger*/) -> bool { + const auto it = payloads_.find(url); + if (it == payloads_.end() || it->second.empty()) { + return false; + } + + if (progress_cb) { + progress_cb(0.0f); + } + + if (cancel_flag && cancel_flag->load()) { + return false; + } + + int& count = call_counts_[url]; + const size_t index = std::min(static_cast(count), it->second.size() - 1); + const auto& bytes = it->second[index]; + count++; + + std::filesystem::create_directories(destination.parent_path()); + std::ofstream out(destination, std::ios::binary); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + + if (progress_cb) { + progress_cb(100.0f); + } + + return true; + }; + } + + private: + std::map>> payloads_; + std::map call_counts_; +}; + +inline std::optional InstallAndFinalize(EpBundleInstaller& installer, + const EpBundleManifest& manifest, ILogger& logger) { + auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); + if (!txn) { + return std::nullopt; + } + + const auto bin_dir = txn->bin_dir(); + if (!txn->Activate(logger)) { + return std::nullopt; + } + + txn->Finalize(logger); + return bin_dir; +} + +inline std::optional InstallAndFinalize(const std::filesystem::path& root, + std::string lock_file_name, std::string display_name, + const EpBundleManifest& manifest, + EpArtifactDownloadFn download_fn, ILogger& logger) { + EpBundleInstaller installer(root, std::move(lock_file_name), std::move(display_name), std::move(download_fn)); + return InstallAndFinalize(installer, manifest, logger); +} + +inline std::optional FindGenerationWithPrefix(const std::filesystem::path& bundles_dir, + std::string_view prefix) { + for (const auto& entry : std::filesystem::directory_iterator(bundles_dir)) { + const auto generation = entry.path().filename().string(); + if (generation.starts_with(prefix)) { + return generation; + } + } + + return std::nullopt; +} + +} // namespace fl::test diff --git a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc index 3c40e75ca..1b80a5d1b 100644 --- a/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc @@ -3,19 +3,16 @@ #include "ep_detection/webgpu_ep_bootstrapper.h" #include "ep_detection/ep_bundle_installer.h" +#include "internal_api/ep_bundle_test_helpers.h" +#include "internal_api/test_helpers.h" #include "logger.h" -#include "util/sha256.h" #include "utils/scoped_environment_variable.h" #include "utils/temp_path.h" #include -#include -#include #include #include -#include -#include #include #include #include @@ -30,75 +27,11 @@ constexpr const char* kOverrideEnv = "FOUNDRY_LOCAL_WEBGPU_EP_LIBRARY"; constexpr const char* kScopedEnvironmentVariableTestEnv = "FOUNDRY_LOCAL_SCOPED_ENVIRONMENT_VARIABLE_TEST"; constexpr const char* kLockFileName = "webgpu-ep.lock"; -class RecordingLogger : public ILogger { - public: - void Log(LogLevel level, std::string_view message) override { - entries.emplace_back(level, std::string(message)); - } - - std::vector> entries; -}; - -std::vector AsBytes(std::string_view text) { return std::vector(text.begin(), text.end()); } - -std::string HashOf(const std::vector& bytes) { - auto tmp = test::TempPath::CreateTempFile("fl_webgpu_bootstrapper_hash_"); - std::ofstream out(tmp.path(), std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - return Sha256File(tmp.path()); -} - -std::string ReadFile(const std::filesystem::path& path) { - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -class FakeDownloads { - public: - void SetSequence(const std::string& url, std::vector> payloads) { - payloads_[url] = std::move(payloads); - } - - EpArtifactDownloadFn AsFn() { - return [this](const std::string& url, const std::filesystem::path& destination, uint64_t /*max_bytes*/, - std::atomic* cancel_flag, const std::function& progress_cb, - ILogger& /*logger*/) -> bool { - auto it = payloads_.find(url); - if (it == payloads_.end() || it->second.empty()) { - return false; - } - - if (progress_cb) { - progress_cb(0.0f); - } - - if (cancel_flag && cancel_flag->load()) { - return false; - } - - int& count = call_counts_[url]; - const size_t index = std::min(static_cast(count), it->second.size() - 1); - const auto& bytes = it->second[index]; - count++; - - std::filesystem::create_directories(destination.parent_path()); - std::ofstream out(destination, std::ios::binary); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - - if (progress_cb) { - progress_cb(100.0f); - } - - return true; - }; - } - - private: - std::map>> payloads_; - std::map call_counts_; -}; +using test::AsBytes; +using test::FakeDownloads; +using test::FindGenerationWithPrefix; +using test::HashOf; +using test::ReadFile; EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string& url) { const auto payload = AsBytes(bundle_id + "-provider"); @@ -119,36 +52,6 @@ EpBundleManifest MakeRawManifest(const std::string& bundle_id, const std::string return manifest; } -std::optional InstallAndFinalize(const std::filesystem::path& root, - const EpBundleManifest& manifest, - EpArtifactDownloadFn download_fn, - ILogger& logger) { - EpBundleInstaller installer(root, kLockFileName, "WebGPU EP", std::move(download_fn)); - auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); - if (!txn) { - return std::nullopt; - } - - const auto bin_dir = txn->bin_dir(); - if (!txn->Activate(logger)) { - return std::nullopt; - } - - txn->Finalize(logger); - return bin_dir; -} - -std::optional FindGenerationWithPrefix(const std::filesystem::path& bundles_dir, std::string_view prefix) { - for (const auto& entry : std::filesystem::directory_iterator(bundles_dir)) { - const auto generation = entry.path().filename().string(); - if (generation.starts_with(prefix)) { - return generation; - } - } - - return std::nullopt; -} - } // namespace TEST(WebGpuEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { @@ -263,7 +166,7 @@ TEST(WebGpuEpBootstrapperTest, BundleActivationFailurePreventsRegistration) { } int registration_count = 0; - RecordingLogger logger; + test::NullLogger logger; WebGpuEpBootstrapper bootstrapper( root.string(), [&](const std::string&, const std::filesystem::path&) { @@ -286,9 +189,10 @@ TEST(WebGpuEpBootstrapperTest, BundleRegistrationFailureRollsBackToPreviousGener downloads.SetSequence("https://example.test/provider-v2.so", {AsBytes("bundle-v2-provider")}); const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/provider-v1.so"); const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/provider-v2.so"); - RecordingLogger logger; + test::NullLogger logger; - ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + ASSERT_TRUE(test::InstallAndFinalize(root.path(), kLockFileName, "WebGPU EP", manifest_v1, downloads.AsFn(), logger) + .has_value()); const auto active_v1 = ReadFile(root.path() / "active"); int registration_count = 0; @@ -319,9 +223,10 @@ TEST(WebGpuEpBootstrapperTest, SuccessfulBundleRegistrationFinalizesPreviousGene downloads.SetSequence("https://example.test/provider-v2.so", {AsBytes("bundle-v2-provider")}); const auto manifest_v1 = MakeRawManifest("bundle-v1", "https://example.test/provider-v1.so"); const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/provider-v2.so"); - RecordingLogger logger; + test::NullLogger logger; - ASSERT_TRUE(InstallAndFinalize(root.path(), manifest_v1, downloads.AsFn(), logger).has_value()); + ASSERT_TRUE(test::InstallAndFinalize(root.path(), kLockFileName, "WebGPU EP", manifest_v1, downloads.AsFn(), logger) + .has_value()); const auto active_v1 = ReadFile(root.path() / "active"); int registration_count = 0; From d60a87d3757ef99496244ed37eb30a90fd8185b4 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Fri, 7 Aug 2026 10:00:41 -0700 Subject: [PATCH 09/11] SetDllDirectoryW for ep bootstrappers --- sdk_v2/cpp/CMakeLists.txt | 9 ++- .../src/ep_detection/cuda_ep_bootstrapper.cc | 70 +++++------------ .../src/ep_detection/cuda_ep_bootstrapper.h | 10 +-- sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h | 3 + .../src/ep_detection/ep_bundle_installer.cc | 76 ++++++++---------- .../src/ep_detection/ep_bundle_installer.h | 11 +-- sdk_v2/cpp/src/ep_detection/ep_detector.cc | 6 ++ sdk_v2/cpp/src/ep_detection/ep_detector.h | 5 ++ sdk_v2/cpp/src/ep_detection/ep_utils.cc | 78 ------------------- sdk_v2/cpp/src/ep_detection/ep_utils.h | 34 -------- .../ep_detection/webgpu_ep_bootstrapper.cc | 43 ++++------ .../src/ep_detection/webgpu_ep_bootstrapper.h | 7 +- .../cpp/src/inferencing/model_load_manager.cc | 31 ++++---- sdk_v2/cpp/src/platform/dynlib_loader.h | 22 ++++++ .../cpp/src/platform/posix/dynlib_loader.cc | 32 ++++++++ .../cpp/src/platform/windows/dynlib_loader.cc | 26 +++++++ sdk_v2/cpp/test/CMakeLists.txt | 1 - .../internal_api/cuda_ep_bootstrapper_test.cc | 13 +--- .../internal_api/ep_bundle_installer_test.cc | 38 ++++----- .../internal_api/ep_bundle_test_helpers.h | 4 +- .../cpp/test/internal_api/ep_detector_test.cc | 16 ++++ sdk_v2/cpp/test/internal_api/ep_utils_test.cc | 34 -------- .../internal_api/model_load_manager_test.cc | 9 +++ 23 files changed, 248 insertions(+), 330 deletions(-) delete mode 100644 sdk_v2/cpp/src/ep_detection/ep_utils.cc delete mode 100644 sdk_v2/cpp/src/ep_detection/ep_utils.h create mode 100644 sdk_v2/cpp/src/platform/dynlib_loader.h create mode 100644 sdk_v2/cpp/src/platform/posix/dynlib_loader.cc create mode 100644 sdk_v2/cpp/src/platform/windows/dynlib_loader.cc delete mode 100644 sdk_v2/cpp/test/internal_api/ep_utils_test.cc diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 29a3a5efd..739ac668c 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -106,6 +106,7 @@ if(WIN32) src/platform/windows/path.cc src/platform/windows/file_io.cc src/platform/windows/cross_process_file_lock.cc + src/platform/windows/dynlib_loader.cc ) else() list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES @@ -114,6 +115,13 @@ else() src/platform/posix/file_io.cc src/platform/posix/cross_process_file_lock.cc ) + # Android does not support the dlopen-based loader; dynlib_loader.cc is + # only compiled for Linux and macOS. + if(NOT ANDROID) + list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES + src/platform/posix/dynlib_loader.cc + ) + endif() endif() # WinML EP bootstrapper is only built when the WinML EP catalog package is @@ -166,7 +174,6 @@ set(FOUNDRY_LOCAL_SOURCES src/ep_detection/cuda_ep_manifest.cc src/ep_detection/ep_bundle_installer.cc src/ep_detection/ep_detector.cc - src/ep_detection/ep_utils.cc src/ep_detection/nvml_gpu_detector.cc src/ep_detection/runtime_version_info.cc src/ep_detection/webgpu_ep_bootstrapper.cc diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 8a2121a84..774d0745e 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -3,9 +3,9 @@ #include "ep_detection/cuda_ep_bootstrapper.h" #include "ep_detection/cuda_ep_manifest.h" -#include "ep_detection/ep_utils.h" #include "ep_detection/nvml_gpu_detector.h" #include "logger.h" +#include "platform/dynlib_loader.h" #include "utils.h" #include @@ -15,10 +15,6 @@ #include #include -#if defined(__linux__) && !defined(__ANDROID__) -#include -#endif - namespace { constexpr const char* kLockFileName = "cuda-ep.lock"; @@ -44,23 +40,6 @@ fl::CudaEpPlatform HostCudaEpPlatform() { } #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) -std::shared_ptr DefaultGenAiCudaLoader(const std::filesystem::path& path, fl::ILogger& logger) { - dlerror(); - void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL); - if (!handle) { - const char* error = dlerror(); - logger.Log(fl::LogLevel::Warning, - fmt::format("CUDA EP: failed to load '{}' ({})", path.string(), error ? error : "unknown error")); - return {}; - } - - return std::shared_ptr(handle, [](void* loaded_library) { - if (loaded_library != nullptr) { - dlclose(loaded_library); - } - }); -} - bool LoadGenAiCudaLibrary( const std::filesystem::path& path, const std::vector>>& loaded_libraries, @@ -74,7 +53,7 @@ bool LoadGenAiCudaLibrary( return true; } - auto loaded_library = loader ? loader(absolute_path, logger) : DefaultGenAiCudaLoader(absolute_path, logger); + auto loaded_library = loader(absolute_path, logger); if (!loaded_library) { return false; } @@ -101,7 +80,7 @@ CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallb installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP", std::move(download_fn)) #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) , - genai_cuda_loader_(genai_cuda_loader ? std::move(genai_cuda_loader) : DefaultGenAiCudaLoader) + genai_cuda_loader_(genai_cuda_loader ? std::move(genai_cuda_loader) : platform::LoadSharedLibrary) #endif { } @@ -142,14 +121,6 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } -#ifdef _WIN32 - EpBundleSearchPathOwner provisional_search_path_owner; - if (!search_path_owner_.Owns(provider_path.parent_path()) && - !provisional_search_path_owner.Add(provider_path.parent_path(), "CUDA EP", logger)) { - return false; - } -#endif - #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) std::pair> provisional_genai_cuda_library; if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_libraries_, @@ -164,10 +135,6 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } -#ifdef _WIN32 - search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); -#endif - #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) if (provisional_genai_cuda_library.second) { genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); @@ -175,6 +142,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& #endif registered_ = true; + bundle_dir_ = provider_path.parent_path(); if (progress_cb) { progress_cb(name_, 100.0f); @@ -197,44 +165,36 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } - if (!txn->Activate(logger)) { + if (!txn->Activate()) { logger.Log(LogLevel::Warning, "CUDA EP: failed to activate bundle"); return false; } const auto provider_path = txn->provider_path(); -#ifdef _WIN32 - EpBundleSearchPathOwner provisional_search_path_owner; - if (!search_path_owner_.Owns(txn->bin_dir()) && - !provisional_search_path_owner.Add(txn->bin_dir(), "CUDA EP", logger)) { - txn->Rollback(logger); - return false; - } -#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) std::pair> provisional_genai_cuda_library; if (!LoadGenAiCudaLibrary(txn->bin_dir() / kGenAiCudaLibrary, genai_cuda_libraries_, genai_cuda_loader_, provisional_genai_cuda_library, logger)) { - txn->Rollback(logger); + txn->Rollback(); return false; } #endif if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "CUDA EP: ORT registration failed"); - txn->Rollback(logger); + txn->Rollback(); return false; } -#ifdef _WIN32 - search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); -#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) if (provisional_genai_cuda_library.second) { genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); } #endif registered_ = true; - txn->Finalize(logger); + bundle_dir_ = txn->bin_dir(); + txn->Finalize(); if (progress_cb) { progress_cb(name_, 100.0f); @@ -248,6 +208,14 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& } } +bool CudaEpBootstrapper::PrepareForModelLoad(ILogger& logger) { +#ifdef _WIN32 + return platform::SetDynamicLibrarySearchDirectory(bundle_dir_, logger); +#else + return true; +#endif +} + bool CudaEpBootstrapper::HasNvidiaGpu(ILogger& logger) { return NvmlGpuDetector::HasNvidiaGpu(logger); } bool CudaEpBootstrapper::IsSupportedPlatform() { diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index 79f8de8aa..3ae24cd93 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -5,8 +5,9 @@ #include "ep_detection/ep_bootstrapper.h" #include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" -#include "ep_detection/ep_utils.h" +#include +#include #include #include #include @@ -35,7 +36,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { , CudaGenAiDependencyLoader genai_cuda_loader = nullptr #endif - ); + ); ~CudaEpBootstrapper() override; // Non-copyable @@ -45,6 +46,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { const std::string& Name() const override; bool IsRegistered() const override; bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) override; + bool PrepareForModelLoad(ILogger& logger) override; /// Check for an NVIDIA GPU with compute capability >= 5.0 using NVML. static bool HasNvidiaGpu(ILogger& logger); @@ -59,9 +61,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { EpRegistrationCallback register_ep_; EpBundleManifestFactory manifest_factory_; EpBundleInstaller installer_; -#ifdef _WIN32 - EpBundleSearchPathOwner search_path_owner_; -#endif + std::filesystem::path bundle_dir_; #if defined(__linux__) && !defined(__ANDROID__) std::vector>> genai_cuda_libraries_; #if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) diff --git a/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h index 41dddd4f3..e1c18260d 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bootstrapper.h @@ -34,6 +34,9 @@ class IEpBootstrapper { virtual bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) = 0; + + /// Prepare process state needed to load a model with this EP. + virtual bool PrepareForModelLoad(ILogger& /*logger*/) { return true; } }; } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc index 322af21fd..a194257d0 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc @@ -40,11 +40,6 @@ namespace { constexpr int kArchiveHashRetries = 1; constexpr int kRawHashRetries = 0; -class NoOpLogger : public ILogger { - public: - void Log(LogLevel /*level*/, std::string_view /*message*/) override {} -}; - class ScopedDirectoryCleanup { public: explicit ScopedDirectoryCleanup(std::filesystem::path path) : path_(std::move(path)) {} @@ -658,7 +653,7 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( return nullptr; } - return std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, + return std::make_unique(logger, std::move(lock), root_dir_, ep_display_name_, manifest, *active_generation, active_bin, active_generation); } @@ -713,8 +708,8 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( return nullptr; } - auto transaction = std::make_unique(std::move(lock), root_dir_, ep_display_name_, manifest, - generation_id, final_bundle_dir / "bin", + auto transaction = std::make_unique(logger, std::move(lock), root_dir_, ep_display_name_, + manifest, generation_id, final_bundle_dir / "bin", active_generation); final_cleanup.Release(); return transaction; @@ -724,11 +719,13 @@ std::unique_ptr EpBundleInstaller::EnsureInstalled( } } -EpInstallTransaction::EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, - std::string ep_display_name, EpBundleManifest manifest, - std::string generation_id, std::filesystem::path bin_dir, +EpInstallTransaction::EpInstallTransaction(ILogger& logger, std::unique_ptr lock, + std::filesystem::path root_dir, std::string ep_display_name, + EpBundleManifest manifest, std::string generation_id, + std::filesystem::path bin_dir, std::optional previous_active_generation) - : lock_(std::move(lock)), + : logger_(logger), + lock_(std::move(lock)), root_dir_(std::move(root_dir)), ep_display_name_(std::move(ep_display_name)), manifest_(std::move(manifest)), @@ -741,10 +738,10 @@ EpInstallTransaction::~EpInstallTransaction() noexcept { return; } - (void)RollbackInternal(/*logger=*/nullptr, /*log_recovery_error=*/false); + (void)RollbackInternal(/*log_recovery_error=*/false); } -bool EpInstallTransaction::Activate(ILogger& logger) { +bool EpInstallTransaction::Activate() { if (activated_ || finalized_) { return true; } @@ -752,14 +749,14 @@ bool EpInstallTransaction::Activate(ILogger& logger) { try { const auto bundles_dir = root_dir_ / "bundles"; const auto staging_root = root_dir_ / "staging"; - if (!ValidateManagedDirectories(bundles_dir, staging_root, ep_display_name_, logger)) { + if (!ValidateManagedDirectories(bundles_dir, staging_root, ep_display_name_, logger_)) { return false; } - if (!VerifyBundleDir(bin_dir_, manifest_, ep_display_name_, logger)) { - logger.Log(LogLevel::Warning, - fmt::format("{}: bundle '{}' failed re-verification before activation; active marker unchanged", - ep_display_name_, manifest_.bundle_id)); + if (!VerifyBundleDir(bin_dir_, manifest_, ep_display_name_, logger_)) { + logger_.Log(LogLevel::Warning, + fmt::format("{}: bundle '{}' failed re-verification before activation; active marker unchanged", + ep_display_name_, manifest_.bundle_id)); return false; } @@ -768,19 +765,19 @@ bool EpInstallTransaction::Activate(ILogger& logger) { return true; } - if (!PublishActiveMarker(root_dir_, generation_id_, ep_display_name_, logger)) { + if (!PublishActiveMarker(root_dir_, generation_id_, ep_display_name_, logger_)) { return false; } activated_ = true; return true; } catch (const std::exception& e) { - logger.Log(LogLevel::Warning, fmt::format("{}: failed to activate bundle: {}", ep_display_name_, e.what())); + logger_.Log(LogLevel::Warning, fmt::format("{}: failed to activate bundle: {}", ep_display_name_, e.what())); return false; } } -void EpInstallTransaction::Finalize(ILogger& logger) { +void EpInstallTransaction::Finalize() { if (finalized_ || !activated_) { return; } @@ -790,45 +787,42 @@ void EpInstallTransaction::Finalize(ILogger& logger) { try { const auto bundles_dir = root_dir_ / "bundles"; const auto staging_root = root_dir_ / "staging"; - (void)CleanupStaleGenerations(bundles_dir, staging_root, {generation_id_}, ep_display_name_, logger); + (void)CleanupStaleGenerations(bundles_dir, staging_root, {generation_id_}, ep_display_name_, logger_); } catch (const std::exception& e) { - logger.Log(LogLevel::Warning, - fmt::format("{}: failed to clean stale bundle generations after activation: {}", ep_display_name_, - e.what())); + logger_.Log(LogLevel::Warning, + fmt::format("{}: failed to clean stale bundle generations after activation: {}", ep_display_name_, + e.what())); } } -bool EpInstallTransaction::Rollback(ILogger& logger) noexcept { +bool EpInstallTransaction::Rollback() noexcept { if (!activated_ || finalized_) { return true; } - return RollbackInternal(&logger, /*log_recovery_error=*/true); + return RollbackInternal(/*log_recovery_error=*/true); } -bool EpInstallTransaction::RollbackInternal(ILogger* logger, bool log_recovery_error) noexcept { +bool EpInstallTransaction::RollbackInternal(bool log_recovery_error) noexcept { if (!activated_ || finalized_) { return true; } - NoOpLogger no_op_logger; - auto& active_logger = logger == nullptr ? static_cast(no_op_logger) : *logger; - try { bool rollback_succeeded = true; if (previous_active_generation_.has_value()) { if (*previous_active_generation_ != generation_id_) { rollback_succeeded = - PublishActiveMarker(root_dir_, *previous_active_generation_, ep_display_name_, active_logger); + PublishActiveMarker(root_dir_, *previous_active_generation_, ep_display_name_, logger_); } } else { rollback_succeeded = - RemoveActiveMarkerIfStillCandidate(root_dir_, generation_id_, ep_display_name_, active_logger); + RemoveActiveMarkerIfStillCandidate(root_dir_, generation_id_, ep_display_name_, logger_); } if (!rollback_succeeded) { - if (logger != nullptr && log_recovery_error) { - LogRollbackRecoveryError(ep_display_name_, root_dir_, *logger); + if (log_recovery_error) { + LogRollbackRecoveryError(ep_display_name_, root_dir_, logger_); } return false; } @@ -836,12 +830,10 @@ bool EpInstallTransaction::RollbackInternal(ILogger* logger, bool log_recovery_e activated_ = false; return true; } catch (const std::exception& e) { - if (logger != nullptr) { - logger->Log(LogLevel::Warning, - fmt::format("{}: failed to roll back active bundle marker: {}", ep_display_name_, e.what())); - if (log_recovery_error) { - LogRollbackRecoveryError(ep_display_name_, root_dir_, *logger); - } + logger_.Log(LogLevel::Warning, + fmt::format("{}: failed to roll back active bundle marker: {}", ep_display_name_, e.what())); + if (log_recovery_error) { + LogRollbackRecoveryError(ep_display_name_, root_dir_, logger_); } return false; diff --git a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h index db76d8998..2b2d9b3d8 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h +++ b/sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h @@ -33,7 +33,7 @@ enum class EpBundleInstallPolicy { class EpInstallTransaction { public: - EpInstallTransaction(std::unique_ptr lock, std::filesystem::path root_dir, + EpInstallTransaction(ILogger& logger, std::unique_ptr lock, std::filesystem::path root_dir, std::string ep_display_name, EpBundleManifest manifest, std::string generation_id, std::filesystem::path bin_dir, std::optional previous_active_generation); ~EpInstallTransaction() noexcept; @@ -48,13 +48,14 @@ class EpInstallTransaction { const std::string& bundle_id() const { return manifest_.bundle_id; } - bool Activate(ILogger& logger); - void Finalize(ILogger& logger); - bool Rollback(ILogger& logger) noexcept; + bool Activate(); + void Finalize(); + bool Rollback() noexcept; private: - bool RollbackInternal(ILogger* logger, bool log_recovery_error) noexcept; + bool RollbackInternal(bool log_recovery_error) noexcept; + ILogger& logger_; std::unique_ptr lock_; std::filesystem::path root_dir_; std::string ep_display_name_; diff --git a/sdk_v2/cpp/src/ep_detection/ep_detector.cc b/sdk_v2/cpp/src/ep_detection/ep_detector.cc index f60e6adf9..143b233d7 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_detector.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_detector.cc @@ -204,4 +204,10 @@ bool EpDetector::IsDownloadInProgress() const { return download_in_progress_; } +bool EpDetector::PrepareForModelLoad(std::string_view ep_name) { + auto it = std::find_if(bootstrappers_.begin(), bootstrappers_.end(), + [&](const auto& bootstrapper) { return bootstrapper->Name() == ep_name; }); + return it == bootstrappers_.end() || (*it)->PrepareForModelLoad(logger_); +} + } // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_detector.h b/sdk_v2/cpp/src/ep_detection/ep_detector.h index 254f1e43e..d63d22abe 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_detector.h +++ b/sdk_v2/cpp/src/ep_detection/ep_detector.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,9 @@ class IEpDetector { /// Whether an EP download/registration operation is currently in progress. /// Default: false. virtual bool IsDownloadInProgress() const { return false; } + + /// Prepare the registered EP for model loading. + virtual bool PrepareForModelLoad(std::string_view /*ep_name*/) { return true; } }; /// Real EP detector that orchestrates bootstrappers for EP discovery and registration. @@ -86,6 +90,7 @@ class EpDetector : public IEpDetector { EpDownloadResult DownloadAndRegisterEps(const std::vector* names, const IEpBootstrapper::ProgressCallback& progress_cb) override; bool IsDownloadInProgress() const override; + bool PrepareForModelLoad(std::string_view ep_name) override; private: const OrtApi& ort_api_; diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc deleted file mode 100644 index e778d3dd7..000000000 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -#include "ep_detection/ep_utils.h" - -#ifdef _WIN32 -#include "logger.h" - -#include - -#include - -#define WIN32_LEAN_AND_MEAN -#include -#endif - -namespace fl { - -#ifdef _WIN32 -namespace { - -std::filesystem::path NormalizeDirectory(const std::filesystem::path& directory) { - return std::filesystem::absolute(directory).lexically_normal(); -} - -} // namespace - -EpBundleSearchPathOwner::~EpBundleSearchPathOwner() { - for (auto it = cookies_.rbegin(); it != cookies_.rend(); ++it) { - if (*it != nullptr) { - RemoveDllDirectory(*it); - } - } -} - -bool EpBundleSearchPathOwner::Owns(const std::filesystem::path& directory) const { - const auto absolute_directory = NormalizeDirectory(directory); - return std::find(directories_.begin(), directories_.end(), absolute_directory) != directories_.end(); -} - -bool EpBundleSearchPathOwner::Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger) { - const auto absolute_directory = NormalizeDirectory(directory); - if (Owns(absolute_directory)) { - return true; - } - - auto* cookie = AddDllDirectory(absolute_directory.c_str()); - if (cookie == nullptr) { - logger.Log(LogLevel::Warning, fmt::format("{}: failed to add DLL search directory '{}' ({})", ep_name, - absolute_directory.string(), GetLastError())); - return false; - } - directories_.push_back(absolute_directory); - cookies_.push_back(cookie); - return true; -} - -void EpBundleSearchPathOwner::MergeFrom(EpBundleSearchPathOwner&& other) noexcept { - for (size_t i = 0; i < other.directories_.size() && i < other.cookies_.size(); ++i) { - if (other.cookies_[i] == nullptr) { - continue; - } - - if (Owns(other.directories_[i])) { - RemoveDllDirectory(other.cookies_[i]); - } else { - directories_.push_back(std::move(other.directories_[i])); - cookies_.push_back(other.cookies_[i]); - } - - other.cookies_[i] = nullptr; - } - - other.directories_.clear(); - other.cookies_.clear(); -} -#endif - -} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h deleted file mode 100644 index c126aba3d..000000000 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -#pragma once - -#include -#include -#include - -namespace fl { - -class ILogger; - -#ifdef _WIN32 -/// Keeps EP bundle directories available to the Windows DLL loader for dependencies loaded after -/// provider registration. Provider libraries themselves are still loaded and owned by ORT. -class EpBundleSearchPathOwner { - public: - EpBundleSearchPathOwner() = default; - ~EpBundleSearchPathOwner(); - - EpBundleSearchPathOwner(const EpBundleSearchPathOwner&) = delete; - EpBundleSearchPathOwner& operator=(const EpBundleSearchPathOwner&) = delete; - - bool Owns(const std::filesystem::path& directory) const; - bool Add(const std::filesystem::path& directory, std::string_view ep_name, ILogger& logger); - void MergeFrom(EpBundleSearchPathOwner&& other) noexcept; - - private: - std::vector directories_; - std::vector cookies_; -}; -#endif - -} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index bb1ec7c67..bb5e4f7fc 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -2,8 +2,8 @@ // Licensed under the MIT License. #include "ep_detection/webgpu_ep_bootstrapper.h" -#include "ep_detection/ep_utils.h" #include "logger.h" +#include "platform/dynlib_loader.h" #include "utils.h" #include @@ -143,25 +143,14 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return false; } -#ifdef _WIN32 - EpBundleSearchPathOwner provisional_search_path_owner; - if (!search_path_owner_.Owns(provider_path.parent_path()) && - !provisional_search_path_owner.Add(provider_path.parent_path(), "WebGPU EP", logger)) { - return false; - } -#endif - if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, fmt::format("WebGPU EP: ORT registration failed for override {}={}", kWebGpuProviderOverrideEnv, provider_path.string())); return false; } -#ifdef _WIN32 - search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); -#endif - registered_ = true; + bundle_dir_ = provider_path.parent_path(); if (progress_cb) { progress_cb(name_, 100.0f); @@ -184,33 +173,21 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac return false; } - if (!txn->Activate(logger)) { + if (!txn->Activate()) { logger.Log(LogLevel::Warning, "WebGPU EP: failed to activate bundle"); return false; } const auto provider_path = txn->provider_path(); -#ifdef _WIN32 - EpBundleSearchPathOwner provisional_search_path_owner; - if (!search_path_owner_.Owns(txn->bin_dir()) && - !provisional_search_path_owner.Add(txn->bin_dir(), "WebGPU EP", logger)) { - txn->Rollback(logger); - return false; - } -#endif - if (!register_ep_(kRegistrationName, provider_path)) { logger.Log(LogLevel::Warning, "WebGPU EP: ORT registration failed"); - txn->Rollback(logger); + txn->Rollback(); return false; } -#ifdef _WIN32 - search_path_owner_.MergeFrom(std::move(provisional_search_path_owner)); -#endif - registered_ = true; - txn->Finalize(logger); + bundle_dir_ = txn->bin_dir(); + txn->Finalize(); if (progress_cb) { progress_cb(name_, 100.0f); @@ -224,6 +201,14 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac } } +bool WebGpuEpBootstrapper::PrepareForModelLoad(ILogger& logger) { +#ifdef _WIN32 + return platform::SetDynamicLibrarySearchDirectory(bundle_dir_, logger); +#else + return true; +#endif +} + bool WebGpuEpBootstrapper::IsSupportedPlatform() { #if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || (defined(__APPLE__) && defined(__aarch64__)) return true; diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index 9049733d7..873222567 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -5,8 +5,8 @@ #include "ep_detection/ep_bootstrapper.h" #include "ep_detection/ep_bundle_installer.h" #include "ep_detection/ep_types.h" -#include "ep_detection/ep_utils.h" +#include #include namespace fl { @@ -32,6 +32,7 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { const std::string& Name() const override; bool IsRegistered() const override; bool DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) override; + bool PrepareForModelLoad(ILogger& logger) override; /// Whether Foundry Local publishes a WebGPU EP bundle for this platform. static bool IsSupportedPlatform(); @@ -43,9 +44,7 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { EpRegistrationCallback register_ep_; EpBundleManifestFactory manifest_factory_; EpBundleInstaller installer_; -#ifdef _WIN32 - EpBundleSearchPathOwner search_path_owner_; -#endif + std::filesystem::path bundle_dir_; }; } // namespace fl diff --git a/sdk_v2/cpp/src/inferencing/model_load_manager.cc b/sdk_v2/cpp/src/inferencing/model_load_manager.cc index 0bc321b9b..a6e2defa8 100644 --- a/sdk_v2/cpp/src/inferencing/model_load_manager.cc +++ b/sdk_v2/cpp/src/inferencing/model_load_manager.cc @@ -132,24 +132,23 @@ ModelLoadManager::LoadResult ModelLoadManager::LoadModel(std::string_view model_ } } - // EP guard: verify the required EP is registered before attempting to load. - // OGA will crash or hang if we try to load a model with an unregistered EP. + std::string_view required_ep; if (resolved_ep != ExecutionProvider::kDefault && resolved_ep != ExecutionProvider::kCPU) { - // Explicit EP resolved — check it directly - auto required = EPUtils::EPtoRegistrationName(resolved_ep); - if (!required.empty() && !HasEP(std::string(required))) { - FL_LOG_AND_THROW(logger_, FOUNDRY_LOCAL_ERROR_INVALID_USAGE, - "model '", id_str, "' requires ", required, - " which is not registered. Call DownloadAndRegisterEps() first."); - } + required_ep = EPUtils::EPtoRegistrationName(resolved_ep); } else { - // No explicit EP — check model_id for device hints - auto required = RequiredEpForModelId(id_str); - if (!required.empty() && !HasEP(std::string(required))) { - FL_LOG_AND_THROW(logger_, FOUNDRY_LOCAL_ERROR_INVALID_USAGE, - "model '", id_str, "' requires ", required, - " which is not registered. Call DownloadAndRegisterEps() first."); - } + required_ep = RequiredEpForModelId(id_str); + } + + // OGA can crash or hang if a model is loaded with an unregistered EP. + if (!required_ep.empty() && !HasEP(std::string(required_ep))) { + FL_LOG_AND_THROW(logger_, FOUNDRY_LOCAL_ERROR_INVALID_USAGE, + "model '", id_str, "' requires ", required_ep, + " which is not registered. Call DownloadAndRegisterEps() first."); + } + + if (!required_ep.empty() && !ep_detector_.PrepareForModelLoad(required_ep)) { + FL_LOG_AND_THROW(logger_, FOUNDRY_LOCAL_ERROR_INTERNAL, + "failed to prepare ", required_ep, " for model loading"); } // std::make_unique cannot access the private constructor; using new directly is intentional. diff --git a/sdk_v2/cpp/src/platform/dynlib_loader.h b/sdk_v2/cpp/src/platform/dynlib_loader.h new file mode 100644 index 000000000..cc32482a3 --- /dev/null +++ b/sdk_v2/cpp/src/platform/dynlib_loader.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include +#include + +namespace fl { +class ILogger; +} + +namespace fl::platform { + +#ifdef _WIN32 +/// Set the directory used for subsequent bare-name dynamic library loads. +bool SetDynamicLibrarySearchDirectory(const std::filesystem::path& directory, ILogger& logger); +#else +/// Load one shared library and keep it resident for the lifetime of the returned handle. +std::shared_ptr LoadSharedLibrary(const std::filesystem::path& path, fl::ILogger& logger); +#endif + +} // namespace fl::platform diff --git a/sdk_v2/cpp/src/platform/posix/dynlib_loader.cc b/sdk_v2/cpp/src/platform/posix/dynlib_loader.cc new file mode 100644 index 000000000..abae41830 --- /dev/null +++ b/sdk_v2/cpp/src/platform/posix/dynlib_loader.cc @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// POSIX shared-library loader — covers Linux and macOS. +// Android is excluded at the build-system level (see CMakeLists.txt), so this +// translation unit is never compiled for Android targets. +#include "platform/dynlib_loader.h" +#include "logger.h" + +#include +#include + +namespace fl::platform { + +std::shared_ptr LoadSharedLibrary(const std::filesystem::path& path, fl::ILogger& logger) { + dlerror(); + void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL); + if (!handle) { + const char* error = dlerror(); + logger.Log(LogLevel::Warning, + fmt::format("EP: failed to load '{}' ({})", path.string(), error ? error : "unknown error")); + return {}; + } + + return std::shared_ptr(handle, [](void* h) { + if (h != nullptr) { + dlclose(h); + } + }); +} + +} // namespace fl::platform diff --git a/sdk_v2/cpp/src/platform/windows/dynlib_loader.cc b/sdk_v2/cpp/src/platform/windows/dynlib_loader.cc new file mode 100644 index 000000000..4c389ba0e --- /dev/null +++ b/sdk_v2/cpp/src/platform/windows/dynlib_loader.cc @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "platform/dynlib_loader.h" +#include "logger.h" + +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +namespace fl::platform { + +bool SetDynamicLibrarySearchDirectory(const std::filesystem::path& directory, ILogger& logger) { + const auto absolute_directory = std::filesystem::absolute(directory).lexically_normal(); + if (SetDllDirectoryW(absolute_directory.c_str())) { + return true; + } + + logger.Log(LogLevel::Warning, + fmt::format("Failed to set DLL search directory '{}' ({})", absolute_directory.string(), GetLastError())); + return false; +} + +} // namespace fl::platform diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index 9ef2d2614..33bf510d5 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -29,7 +29,6 @@ add_executable(foundry_local_tests internal_api/embeddings/fp16_test.cc internal_api/ep_bundle_installer_test.cc internal_api/ep_detector_test.cc - internal_api/ep_utils_test.cc internal_api/exception_test.cc internal_api/execution_provider_test.cc internal_api/file_lock_test.cc diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index fe1bcfa83..a7ae38d09 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -4,7 +4,6 @@ #include "ep_detection/cuda_ep_manifest.h" #include "ep_detection/ep_bundle_installer.h" -#include "ep_detection/ep_utils.h" #include "internal_api/ep_bundle_test_helpers.h" #include "internal_api/test_helpers.h" #include "logger.h" @@ -134,12 +133,9 @@ CudaEpBootstrapper MakeInstalledBundleBootstrapper(std::string root_dir, Registe }); }; - return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), - [manifest] { return std::optional(manifest); }, std::move(download_fn), - loader); + return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), [manifest] { return std::optional(manifest); }, std::move(download_fn), loader); #else - return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), - [manifest] { return std::optional(manifest); }, std::move(download_fn)); + return CudaEpBootstrapper(std::move(root_dir), std::move(register_ep), [manifest] { return std::optional(manifest); }, std::move(download_fn)); #endif } #endif @@ -404,7 +400,7 @@ TEST(CudaEpBootstrapperTest, InstalledBundleGenAiDependencyLoaderFailureRollsBac ASSERT_EQ(requested_paths.size(), 1u); EXPECT_EQ(requested_paths[0], std::filesystem::absolute(root.path() / "bundles" / *candidate_v2 / "bin" / kGenAiCudaLibrary) - .lexically_normal()); + .lexically_normal()); } TEST(CudaEpBootstrapperTest, OverrideRegistrationFailureDoesNotPoisonGenAiCudaRetryState) { @@ -434,8 +430,7 @@ TEST(CudaEpBootstrapperTest, OverrideRegistrationFailureDoesNotPoisonGenAiCudaRe CudaEpBootstrapper bootstrapper(root.string(), [&](const std::string&, const std::filesystem::path&) { ++registration_count; - return allow_registration; - }, + return allow_registration; }, /*manifest_factory=*/nullptr, /*download_fn=*/nullptr, loader); StderrLogger logger; diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc index 217ebccf7..955f35618 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc @@ -128,8 +128,8 @@ TEST(EpBundleInstallerTest, ReusesValidBundleWithoutRedownloading) { auto first = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(first, nullptr); first_bin = first->bin_dir(); - ASSERT_TRUE(first->Activate(logger)); - first->Finalize(logger); + ASSERT_TRUE(first->Activate()); + first->Finalize(); } auto second = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); @@ -300,7 +300,7 @@ TEST(EpBundleInstallerTest, ActivateWritesActiveMarkerFile) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - EXPECT_TRUE(txn->Activate(logger)); + EXPECT_TRUE(txn->Activate()); EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-42-")); } @@ -321,7 +321,7 @@ TEST(EpBundleInstallerTest, ActivateReplacesExistingActiveMarker) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - EXPECT_TRUE(txn->Activate(logger)); + EXPECT_TRUE(txn->Activate()); EXPECT_TRUE(ReadFile(root.path() / "active").starts_with("bundle-1-")) << "publishing replaces an existing active marker atomically"; } @@ -368,7 +368,7 @@ TEST(EpBundleInstallerTest, ActivateRevalidatesUnderLockAndRefusesTamperedBundle std::ofstream(txn->bin_dir() / "unexpected.txt") << "surprise"; } - EXPECT_FALSE(txn->Activate(logger)) << "re-verification under the lock must reject a tampered bundle"; + EXPECT_FALSE(txn->Activate()) << "re-verification under the lock must reject a tampered bundle"; EXPECT_EQ(ReadFile(root.path() / "active"), active_v1) << "a bundle failing re-verification must not advance the active marker"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) @@ -400,7 +400,7 @@ TEST(EpBundleInstallerTest, ActivatePreservesPreviousMarkerWhenPublicationFails) std::ofstream(root.path() / "active" / "blocker") << "x"; } - EXPECT_FALSE(txn->Activate(logger)) << "a failed marker publication is reported as failure"; + EXPECT_FALSE(txn->Activate()) << "a failed marker publication is reported as failure"; EXPECT_TRUE(std::filesystem::is_directory(root.path() / "active")) << "the failed publication left the marker path untouched"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) @@ -427,14 +427,14 @@ TEST(EpBundleInstallerTest, FinalizeRemovesOldGenerationsOnlyAfterSuccessfulActi auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); const auto active_v2 = ReadFile(root.path() / "active"); EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)) << "activation must not delete the previously active generation"; EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v2)); - txn->Finalize(logger); + txn->Finalize(); EXPECT_FALSE(std::filesystem::exists(root.path() / "bundles" / active_v1)) << "finalization removes the previous generation only after registration succeeds"; @@ -458,11 +458,11 @@ TEST(EpBundleInstallerTest, RollbackRestoresPreviousMarkerAndRetainsGenerations) const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); ASSERT_NE(ReadFile(root.path() / "active"), active_v1); const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); - EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_TRUE(txn->Rollback()); EXPECT_EQ(ReadFile(root.path() / "active"), active_v1); EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); @@ -479,12 +479,12 @@ TEST(EpBundleInstallerTest, RollbackFirstInstallRemovesCandidateMarkerAndRetains const auto manifest = MakeRawManifest("bundle-v1", "https://example.test/v1.so", HashOf(payload)); auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); ASSERT_EQ(ReadFile(root.path() / "active"), candidate_generation); - EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_TRUE(txn->Rollback()); EXPECT_FALSE(std::filesystem::exists(root.path() / "active")); EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / candidate_generation)); } @@ -503,10 +503,10 @@ TEST(EpBundleInstallerTest, RollbackOnReusedActiveBundleLeavesMarkerUnchanged) { auto txn = installer.EnsureInstalled(manifest, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); ASSERT_EQ(ReadFile(root.path() / "active"), active_generation); - EXPECT_TRUE(txn->Rollback(logger)); + EXPECT_TRUE(txn->Rollback()); EXPECT_EQ(ReadFile(root.path() / "active"), active_generation); } @@ -527,7 +527,7 @@ TEST(EpBundleInstallerTest, ActivatedTransactionDestructorRestoresPreviousMarker const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); const auto lock_path = root.path() / "test.lock"; EXPECT_THROW({ FileLock probe(lock_path, /*timeout_ms=*/0); }, std::runtime_error); @@ -555,7 +555,7 @@ TEST(EpBundleInstallerTest, RollbackFailureLogsCacheRecoveryMessageAndRetainsGen const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); const auto candidate_generation = txn->bin_dir().parent_path().filename().string(); std::filesystem::remove(root.path() / "active"); @@ -564,7 +564,7 @@ TEST(EpBundleInstallerTest, RollbackFailureLogsCacheRecoveryMessageAndRetainsGen std::ofstream(root.path() / "active" / "blocker") << "x"; } - EXPECT_FALSE(txn->Rollback(logger)); + EXPECT_FALSE(txn->Rollback()); EXPECT_TRUE(logger.Contains("failed to recover the active bundle marker after bootstrap failure")); EXPECT_TRUE(logger.Contains(root.path().string())); EXPECT_TRUE(std::filesystem::exists(root.path() / "bundles" / active_v1)); @@ -588,7 +588,7 @@ TEST(EpBundleInstallerTest, FinalizeCleanupFailureKeepsPublishedMarker) { const auto manifest_v2 = MakeRawManifest("bundle-v2", "https://example.test/v2.so", HashOf(payload_v2)); auto txn = installer.EnsureInstalled(manifest_v2, /*progress_cb=*/nullptr, logger); ASSERT_NE(txn, nullptr); - ASSERT_TRUE(txn->Activate(logger)); + ASSERT_TRUE(txn->Activate()); const auto active_v2 = ReadFile(root.path() / "active"); std::filesystem::rename(root.path() / "bundles", root.path() / "bundles_saved"); @@ -596,7 +596,7 @@ TEST(EpBundleInstallerTest, FinalizeCleanupFailureKeepsPublishedMarker) { std::ofstream(root.path() / "bundles") << "block cleanup"; } - txn->Finalize(logger); + txn->Finalize(); txn.reset(); EXPECT_EQ(ReadFile(root.path() / "active"), active_v2); diff --git a/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h b/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h index eb312c01c..f3f2efd8e 100644 --- a/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h +++ b/sdk_v2/cpp/test/internal_api/ep_bundle_test_helpers.h @@ -98,11 +98,11 @@ inline std::optional InstallAndFinalize(EpBundleInstaller } const auto bin_dir = txn->bin_dir(); - if (!txn->Activate(logger)) { + if (!txn->Activate()) { return std::nullopt; } - txn->Finalize(logger); + txn->Finalize(); return bin_dir; } diff --git a/sdk_v2/cpp/test/internal_api/ep_detector_test.cc b/sdk_v2/cpp/test/internal_api/ep_detector_test.cc index 8feaef302..d6d54ca07 100644 --- a/sdk_v2/cpp/test/internal_api/ep_detector_test.cc +++ b/sdk_v2/cpp/test/internal_api/ep_detector_test.cc @@ -55,7 +55,14 @@ class MockEpBootstrapper : public IEpBootstrapper { return succeed_; } + bool PrepareForModelLoad(ILogger&) override { + prepare_called_ = true; + return prepare_succeeds_; + } + bool download_called_ = false; + bool prepare_called_ = false; + bool prepare_succeeds_ = true; private: std::string name_; @@ -106,6 +113,15 @@ TEST_F(EpDetectorTest, GetAvailableDevices_AlwaysIncludesCpu) { EXPECT_FALSE(devices.at("CPU").empty()); } +TEST_F(EpDetectorTest, PrepareForModelLoad_DelegatesToMatchingBootstrapper) { + std::vector mocks; + auto detector = MakeDetector(mocks, {{"CUDAExecutionProvider", true}, {"WebGpuExecutionProvider", true}}); + + EXPECT_TRUE(detector->PrepareForModelLoad("WebGpuExecutionProvider")); + EXPECT_FALSE(mocks[0]->prepare_called_); + EXPECT_TRUE(mocks[1]->prepare_called_); +} + TEST_F(EpDetectorTest, DownloadAll_CallsAllBootstrappers) { std::vector mocks; auto detector = MakeDetector(mocks, {{"CUDAExecutionProvider", true}, diff --git a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc b/sdk_v2/cpp/test/internal_api/ep_utils_test.cc deleted file mode 100644 index cdb2dcde0..000000000 --- a/sdk_v2/cpp/test/internal_api/ep_utils_test.cc +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -#include "ep_detection/ep_utils.h" - -#ifdef _WIN32 -#include "logger.h" -#include "utils/temp_path.h" - -#include - -#include - -namespace fl { - -namespace { - -class NullLogger : public ILogger { - public: - void Log(LogLevel /*level*/, std::string_view /*message*/) override {} -}; - -} // namespace - -TEST(EpUtilsTest, SearchPathOwnerAcceptsExistingDirectoryAndDuplicateAdd) { - auto directory = test::TempPath::CreateTempDir("fl_ep_search_path_"); - NullLogger logger; - EpBundleSearchPathOwner owner; - - EXPECT_TRUE(owner.Add(directory.path(), "Test EP", logger)); - EXPECT_TRUE(owner.Add(directory.path(), "Test EP", logger)); -} - -} // namespace fl -#endif diff --git a/sdk_v2/cpp/test/internal_api/model_load_manager_test.cc b/sdk_v2/cpp/test/internal_api/model_load_manager_test.cc index 695f0e5d7..c17817b9c 100644 --- a/sdk_v2/cpp/test/internal_api/model_load_manager_test.cc +++ b/sdk_v2/cpp/test/internal_api/model_load_manager_test.cc @@ -26,6 +26,13 @@ class GpuEpDetector : public fl::IEpDetector { {"GPU", {"CUDAExecutionProvider"}}, }; } + + bool PrepareForModelLoad(std::string_view ep_name) override { + prepared_ep = ep_name; + return true; + } + + std::string prepared_ep; }; /// EP detector that reports CPU only. @@ -168,6 +175,8 @@ TEST(ModelLoadManagerTest, LoadGenericGpuModel_CudaAvailable_AutoSelectsCuda) { // Should NOT be an EP guard error EXPECT_NE(e.code(), FOUNDRY_LOCAL_ERROR_INVALID_USAGE); } + + EXPECT_EQ(ep.prepared_ep, "CUDAExecutionProvider"); } // --------------------------------------------------------------------------- From a52083aec5e81e4927537656af0a2138c118884f Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Fri, 7 Aug 2026 18:40:22 +0000 Subject: [PATCH 10/11] Address pipeline failures --- sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc | 2 +- sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 774d0745e..42fad54eb 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -208,7 +208,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& } } -bool CudaEpBootstrapper::PrepareForModelLoad(ILogger& logger) { +bool CudaEpBootstrapper::PrepareForModelLoad([[maybe_unused]] ILogger& logger) { #ifdef _WIN32 return platform::SetDynamicLibrarySearchDirectory(bundle_dir_, logger); #else diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index bb5e4f7fc..40c5ee457 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -201,7 +201,7 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallbac } } -bool WebGpuEpBootstrapper::PrepareForModelLoad(ILogger& logger) { +bool WebGpuEpBootstrapper::PrepareForModelLoad([[maybe_unused]] ILogger& logger) { #ifdef _WIN32 return platform::SetDynamicLibrarySearchDirectory(bundle_dir_, logger); #else From bc01d6001813205fb05e21c2b84ff780ae2a1080 Mon Sep 17 00:00:00 2001 From: Baiju Meswani Date: Fri, 7 Aug 2026 21:49:16 +0000 Subject: [PATCH 11/11] Exclude compiling ep bootstrappers on android and linux arm64 --- sdk_v2/cpp/CMakeLists.txt | 27 +++++++++++++++---- .../src/ep_detection/cuda_ep_bootstrapper.cc | 23 +++++++--------- .../src/ep_detection/cuda_ep_bootstrapper.h | 8 +++--- sdk_v2/cpp/src/manager.cc | 12 +++++++-- sdk_v2/cpp/test/CMakeLists.txt | 13 ++++++--- .../internal_api/cuda_ep_bootstrapper_test.cc | 17 +++++------- 6 files changed, 61 insertions(+), 39 deletions(-) diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 739ac668c..457ef9da6 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -135,6 +135,24 @@ if(WinMLEpCatalog_FOUND) ) endif() +string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" FOUNDRY_LOCAL_SYSTEM_PROCESSOR) +set(FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS ON) +if(ANDROID OR + (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND + NOT FOUNDRY_LOCAL_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64)$")) + set(FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS OFF) +endif() + +if(FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS) + list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES + src/ep_detection/cuda_ep_bootstrapper.cc + src/ep_detection/cuda_ep_manifest.cc + src/ep_detection/ep_bundle_installer.cc + src/ep_detection/nvml_gpu_detector.cc + src/ep_detection/webgpu_ep_bootstrapper.cc + ) +endif() + if(ANDROID) list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES src/platform/android/ssl_cert_checker.cc) endif() @@ -170,13 +188,8 @@ set(FOUNDRY_LOCAL_SOURCES src/download/file_writer.cc src/download/inference_model_writer.cc src/download/model_registry_client.cc - src/ep_detection/cuda_ep_bootstrapper.cc - src/ep_detection/cuda_ep_manifest.cc - src/ep_detection/ep_bundle_installer.cc src/ep_detection/ep_detector.cc - src/ep_detection/nvml_gpu_detector.cc src/ep_detection/runtime_version_info.cc - src/ep_detection/webgpu_ep_bootstrapper.cc src/exception.cc src/inferencing/generative/genai_config.cc src/http/http_client.cc @@ -282,6 +295,10 @@ function(foundry_local_configure_target TARGET LINK_SCOPE) if(ANDROID) target_link_libraries(${TARGET} ${LINK_SCOPE} log) endif() + target_compile_definitions( + ${TARGET} + PRIVATE FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS=$ + ) if(FOUNDRY_LOCAL_BUILD_SERVICE) target_link_libraries(${TARGET} ${LINK_SCOPE} oatpp::oatpp) diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 42fad54eb..7bd445fb5 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -21,7 +21,7 @@ constexpr const char* kLockFileName = "cuda-ep.lock"; constexpr int kMaxInstallAttempts = 5; constexpr const char* kRegistrationName = "CUDAExecutionProvider"; constexpr const char* kCudaProviderOverrideEnv = "FOUNDRY_LOCAL_CUDA_EP_LIBRARY"; -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) constexpr const char* kGenAiCudaLibrary = "libonnxruntime-genai-cuda.so"; #endif @@ -30,16 +30,14 @@ fl::CudaEpPlatform HostCudaEpPlatform() { return fl::CudaEpPlatform::WindowsArm64; #elif defined(_WIN32) && defined(_M_X64) return fl::CudaEpPlatform::WindowsX64; -#elif defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#elif defined(__linux__) return fl::CudaEpPlatform::LinuxX64; -#elif defined(__linux__) && defined(__aarch64__) && !defined(__ANDROID__) - return fl::CudaEpPlatform::LinuxArm64; #else return fl::CudaEpPlatform::Unsupported; #endif } -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) bool LoadGenAiCudaLibrary( const std::filesystem::path& path, const std::vector>>& loaded_libraries, @@ -69,7 +67,7 @@ namespace fl { CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, EpBundleManifestFactory manifest_factory, EpArtifactDownloadFn download_fn -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) , CudaGenAiDependencyLoader genai_cuda_loader #endif @@ -78,7 +76,7 @@ CudaEpBootstrapper::CudaEpBootstrapper(std::string root_dir, EpRegistrationCallb manifest_factory_(manifest_factory ? std::move(manifest_factory) : [] { return BuildCudaEpManifest(HostCudaEpPlatform()); }), installer_(std::filesystem::path(root_dir), kLockFileName, "CUDA EP", std::move(download_fn)) -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) , genai_cuda_loader_(genai_cuda_loader ? std::move(genai_cuda_loader) : platform::LoadSharedLibrary) #endif @@ -121,7 +119,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) std::pair> provisional_genai_cuda_library; if (!LoadGenAiCudaLibrary(provider_path.parent_path() / kGenAiCudaLibrary, genai_cuda_libraries_, genai_cuda_loader_, provisional_genai_cuda_library, logger)) { @@ -135,7 +133,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) if (provisional_genai_cuda_library.second) { genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); } @@ -171,7 +169,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& } const auto provider_path = txn->provider_path(); -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) std::pair> provisional_genai_cuda_library; if (!LoadGenAiCudaLibrary(txn->bin_dir() / kGenAiCudaLibrary, genai_cuda_libraries_, genai_cuda_loader_, provisional_genai_cuda_library, logger)) { @@ -186,7 +184,7 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& return false; } -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) if (provisional_genai_cuda_library.second) { genai_cuda_libraries_.push_back(std::move(provisional_genai_cuda_library)); } @@ -219,8 +217,7 @@ bool CudaEpBootstrapper::PrepareForModelLoad([[maybe_unused]] ILogger& logger) { bool CudaEpBootstrapper::HasNvidiaGpu(ILogger& logger) { return NvmlGpuDetector::HasNvidiaGpu(logger); } bool CudaEpBootstrapper::IsSupportedPlatform() { -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || defined(__linux__) return true; #else return false; diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h index 3ae24cd93..9b5ab915f 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h @@ -17,7 +17,7 @@ namespace fl { class ILogger; -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) using CudaGenAiDependencyLoader = std::function(const std::filesystem::path&, ILogger&)>; #endif @@ -32,7 +32,7 @@ class CudaEpBootstrapper : public IEpBootstrapper { CudaEpBootstrapper(std::string root_dir, EpRegistrationCallback register_ep, EpBundleManifestFactory manifest_factory = nullptr, EpArtifactDownloadFn download_fn = nullptr -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) , CudaGenAiDependencyLoader genai_cuda_loader = nullptr #endif @@ -62,12 +62,10 @@ class CudaEpBootstrapper : public IEpBootstrapper { EpBundleManifestFactory manifest_factory_; EpBundleInstaller installer_; std::filesystem::path bundle_dir_; -#if defined(__linux__) && !defined(__ANDROID__) +#if defined(__linux__) std::vector>> genai_cuda_libraries_; -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) CudaGenAiDependencyLoader genai_cuda_loader_; #endif -#endif }; } // namespace fl diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index 0612b9745..bf4ef2561 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -12,11 +12,15 @@ #include "catalog.h" #include "catalog/azure_model_catalog.h" #include "download/download_manager.h" +#if FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS #include "ep_detection/cuda_ep_bootstrapper.h" +#endif #include "ep_detection/ep_detector.h" #include "ep_detection/ep_types.h" #include "ep_detection/runtime_version_info.h" +#if FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS #include "ep_detection/webgpu_ep_bootstrapper.h" +#endif #include "exception.h" #include "inferencing/model_load_manager.h" #include "inferencing/session/session_manager.h" @@ -192,8 +196,6 @@ Manager::Manager(const Configuration& config) : config_(config) { CheckSslCertSetup(*logger_); #endif - // Build the EP registration callback. When a bootstrapper successfully - // prepares an EP, this callback registers it with ORT via the C API. // OrtEnv is a singleton — CreateEnv returns the existing instance if GenAI // (or any other ORT consumer) already created one, with a bumped refcount. // We own one refcount and release it (plus unregister each EP we registered) @@ -216,6 +218,9 @@ Manager::Manager(const Configuration& config) : config_(config) { LogRuntimeVersions(*logger_); +#if FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS + // Build the EP registration callback. When a bootstrapper successfully + // prepares an EP, this callback registers it with ORT via the C API. EpRegistrationCallback register_ep = [this, &log = *logger_](const std::string& registration_name, const std::filesystem::path& library_path) -> bool { OrtStatus* status = @@ -236,10 +241,12 @@ Manager::Manager(const Configuration& config) : config_(config) { ", version=" + version + ")"); return true; }; +#endif // Discover bootstrappers from available EP sources std::vector> bootstrappers; +#if FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS // Detected once and reused below for the WinML catalog skip-list and CUDA bootstrapper. // Avoid probing NVML on platforms where Foundry Local does not publish a CUDA bundle. const bool has_nvidia_gpu = @@ -268,6 +275,7 @@ Manager::Manager(const Configuration& config) : config_(config) { const auto webgpu_ep_root = std::filesystem::path(*config_.app_data_dir) / "ep" / "webgpu-ep"; bootstrappers.push_back(std::make_unique(webgpu_ep_root.string(), register_ep)); } +#endif ep_detector_ = std::make_unique(*ort_api_, *ort_env_, std::move(bootstrappers), *logger_); diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index 33bf510d5..51b9524d5 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -23,11 +23,9 @@ add_executable(foundry_local_tests internal_api/chat_completions_converter_test.cc internal_api/configuration_test.cc internal_api/cross_process_file_lock_test.cc - internal_api/cuda_ep_bootstrapper_test.cc internal_api/download_test.cc internal_api/embeddings/contracts_embeddings_test.cc internal_api/embeddings/fp16_test.cc - internal_api/ep_bundle_installer_test.cc internal_api/ep_detector_test.cc internal_api/exception_test.cc internal_api/execution_provider_test.cc @@ -44,7 +42,6 @@ add_executable(foundry_local_tests internal_api/model_io_info_test.cc internal_api/model_load_manager_test.cc internal_api/model_sorting_test.cc - internal_api/nvml_gpu_detector_test.cc internal_api/platform_path_test.cc internal_api/response_converter_test.cc internal_api/response_store_test.cc @@ -62,11 +59,19 @@ add_executable(foundry_local_tests internal_api/toolcalling/grammar_test.cc internal_api/utils_test.cc internal_api/web_service_test.cc - internal_api/webgpu_ep_bootstrapper_test.cc internal_api/winml_provider_allowlist_test.cc internal_api/zip_extract_test.cc ) +if(FOUNDRY_LOCAL_HAS_EP_BOOTSTRAPPERS) + target_sources(foundry_local_tests PRIVATE + internal_api/cuda_ep_bootstrapper_test.cc + internal_api/ep_bundle_installer_test.cc + internal_api/nvml_gpu_detector_test.cc + internal_api/webgpu_ep_bootstrapper_test.cc + ) +endif() + target_compile_options(foundry_local_tests PRIVATE ${FOUNDRY_LOCAL_COMPILE_OPTIONS}) target_link_libraries(foundry_local_tests diff --git a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc index a7ae38d09..0654893c5 100644 --- a/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc +++ b/sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc @@ -70,15 +70,14 @@ using test::HashOf; using test::NullLogger; using test::ReadFile; -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || defined(__linux__) constexpr const char* kLockFileName = "cuda-ep.lock"; #if defined(_WIN32) constexpr const char* kProviderRelativePath = "onnxruntime_providers_cuda.dll"; #else constexpr const char* kProviderRelativePath = "libonnxruntime_providers_cuda.so"; #endif -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) constexpr const char* kGenAiCudaLibrary = "libonnxruntime-genai-cuda.so"; #endif @@ -102,7 +101,7 @@ EpBundleManifest MakeRawManifest(FakeDownloads& downloads, const std::string& bu .raw_sha256 = HashOf(provider_payload), .raw_max_bytes = 1024}}; -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) const auto genai_url = std::string("https://example.test/") + bundle_id + "/genai"; const auto genai_payload = AsBytes(bundle_id + "-genai"); @@ -126,7 +125,7 @@ EpBundleManifest MakeRawManifest(FakeDownloads& downloads, const std::string& bu template CudaEpBootstrapper MakeInstalledBundleBootstrapper(std::string root_dir, RegisterEp register_ep, const EpBundleManifest& manifest, EpArtifactDownloadFn download_fn) { -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) auto loader = [](const std::filesystem::path&, ILogger&) -> std::shared_ptr { return std::shared_ptr(new int(0), [](void* token) { delete static_cast(token); @@ -143,8 +142,7 @@ CudaEpBootstrapper MakeInstalledBundleBootstrapper(std::string root_dir, Registe } // namespace TEST(CudaEpBootstrapperTest, PlatformSupportMatchesPublishedBundles) { -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || defined(__linux__) EXPECT_TRUE(CudaEpBootstrapper::IsSupportedPlatform()); #else EXPECT_FALSE(CudaEpBootstrapper::IsSupportedPlatform()); @@ -266,8 +264,7 @@ TEST(CudaEpBootstrapperTest, OverrideCancellationBeforeRegistrationReturnsFalse) EXPECT_EQ(registration_count, 0); } -#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || \ - (defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__)) +#if (defined(_WIN32) && (defined(_M_ARM64) || defined(_M_X64))) || defined(__linux__) TEST(CudaEpBootstrapperTest, BundleActivationFailurePreventsRegistration) { auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); FakeDownloads downloads; @@ -360,7 +357,7 @@ TEST(CudaEpBootstrapperTest, SuccessfulBundleRegistrationFinalizesPreviousGenera } #endif -#if defined(__linux__) && defined(__x86_64__) && !defined(__ANDROID__) +#if defined(__linux__) TEST(CudaEpBootstrapperTest, InstalledBundleGenAiDependencyLoaderFailureRollsBackMarkerAndSkipsRegistration) { auto root = test::TempPath::CreateTempDir("fl_cuda_bootstrapper_"); FakeDownloads downloads;