已合并
fix(security): require trusted verified model downloads #141
写代码啥时候能换包餐巾纸啊创建于 8月11日
fix(security): require trusted verified model downloads #141
已合并
写代码啥时候能换包餐巾纸啊创建于 8月11日
30 个文件变更+1799-183
@@ -70,6 +70,20 @@ endif()
70option(SMARTSERVE_BUILD_APPLICATIONS "Build SmartServe sample applications" ON)70option(SMARTSERVE_BUILD_APPLICATIONS "Build SmartServe sample applications" ON)
71option(SMARTSERVE_BUILD_TESTS "Build SmartServe tests" ON)71option(SMARTSERVE_BUILD_TESTS "Build SmartServe tests" ON)
72option(SMARTSERVE_ENABLE_PERF_STATS "Enable performance statistics (model load, prefill, decode)" OFF)72option(SMARTSERVE_ENABLE_PERF_STATS "Enable performance statistics (model load, prefill, decode)" OFF)
73+option(SMARTSERVE_ENABLE_TEST_FILE_URLS
hb
hbhb8月18日

新增选项 SMARTSERVE_ENABLE_TEST_FILE_URLS 默认关闭

原有下载、暂停恢复、并发、删除及元数据测试现在通过 GTEST_SKIP() 跳过

项目标准入口 build_and_test.sh 没有启用该选项,因此默认回归会成功退出,却不再覆盖核心下载成功路径。

此次提交共给 17 个测试增加了该跳过条件。

新测试传输开关没有接入标准测试入口,也没有记录启用方式,导致默认回归静默跳过下载成功、恢复、并发和删除路径。

建议提供专用的安全测试目标或测试 transport 注入,使标准 CI 能运行这些用例,同时不让生产库支持本地文件 URL。

likedislike
8月18日 评论:
74+ "UNSAFE: allow file+test:// model downloads for isolated download tests" OFF)
75+option(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS
76+ "UNSAFE: allow preinstalled model files in test/runtime-smoke builds" OFF)
77+ 
78+if(SMARTSERVE_ENABLE_TEST_FILE_URLS AND NOT SMARTSERVE_BUILD_TESTS)
79+ message(FATAL_ERROR
80+ "SMARTSERVE_ENABLE_TEST_FILE_URLS is test-only and requires SMARTSERVE_BUILD_TESTS=ON")
81+endif()
82+if(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS AND
83+ NOT (SMARTSERVE_BUILD_TESTS OR SMARTSERVE_BUILD_APPLICATIONS))
84+ message(FATAL_ERROR
85+ "SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS requires a test or application smoke build")
86+endif()
73 87 
74# 基本依赖88# 基本依赖
75find_package(Threads REQUIRED)89find_package(Threads REQUIRED)
@@ -216,6 +230,15 @@ add_library(smartserve_core
216target_compile_definitions(smartserve_core PUBLIC230target_compile_definitions(smartserve_core PUBLIC
217 "SMART_SERVE_CONFIG_JSON_PATH=\"${SMART_SERVE_CONFIG_JSON_ABS}\"")231 "SMART_SERVE_CONFIG_JSON_PATH=\"${SMART_SERVE_CONFIG_JSON_ABS}\"")
218 232 
233+# Local-file downloads are an explicit opt-in for isolated test builds. The
234+# standard test entrypoint enables this only for its non-release build tree.
235+if(SMARTSERVE_ENABLE_TEST_FILE_URLS)
236+ target_compile_definitions(smartserve_core PRIVATE SMARTSERVE_TEST_FILE_URLS_ENABLED)
237+endif()
238+if(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS)
239+ target_compile_definitions(smartserve_core PRIVATE SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
240+endif()
241+ 
219target_link_libraries(smartserve_core PUBLIC smartserve_utils nlohmann_json::nlohmann_json Threads::Threads CURL::libcurl)242target_link_libraries(smartserve_core PUBLIC smartserve_utils nlohmann_json::nlohmann_json Threads::Threads CURL::libcurl)
220 243 
221# plugin/include/task.h exposes llama.cpp chat template types in inline helpers,244# plugin/include/task.h exposes llama.cpp chat template types in inline helpers,
@@ -9,7 +9,7 @@
9- C++17 编译器:Apple Clang 14+、Clang 12+ 或 GCC 10+9- C++17 编译器:Apple Clang 14+、Clang 12+ 或 GCC 10+
10- Git(用于按需初始化 submodule)10- Git(用于按需初始化 submodule)
11 11 
12-脚本默认把构建产物放在 `build/macos` 或 `build/linux`。需要模型推理或应用冒烟时,还需 `SMART_SERVE_MODEL_DIR` 中准备模型12+脚本默认把构建产物放在 `build/macos` 或 `build/linux`。需要模型推理或应用冒烟时,还需设置应用私有的 `SMART_SERVE_MODEL_DIR`。
13 13 
14## 2. 首次准备14## 2. 首次准备
15 15 
@@ -52,6 +52,8 @@ SMART_SERVE_MODEL_DIR=/path/to/models \
52 52 
53运行时测试的模型目录默认为 `$HOME/.cache/models``--skip-deps-download``--skip-deps-build``--skip-cmake``--skip-build``--skip-tests` 适合已有构建目录的调试,不建议作为首次构建命令。53运行时测试的模型目录默认为 `$HOME/.cache/models``--skip-deps-download``--skip-deps-build``--skip-cmake``--skip-build``--skip-tests` 适合已有构建目录的调试,不建议作为首次构建命令。
54 54 
55+生产构建不会加载手动复制到模型目录的预置权重;模型必须通过 SDK 下载 API 写入并生成已验证的 `installed_models.json` 记录后才能进入推理引擎。`SMART_SERVE_MODEL_DIR` 必须是应用私有、不会被其他主体写入的目录。真实模型运行时测试和应用冒烟会显式启用仅测试用的预置模型开关,以便复用本地测试权重;手动 CMake 测试构建可使用 `-DSMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS=ON`,该开关绝不能用于发布产物。
56+ 
55## 4. 构建并运行应用57## 4. 构建并运行应用
56 58 
57```bash59```bash
@@ -114,7 +116,7 @@ OpenHarmony 系统服务不受此迁移影响,仍使用 `/data/local/tmp/model
114|------|------|116|------|------|
115| `LLAMACPP_ROOT` | 已构建 llama.cpp 的源码/构建根目录;设置后不会编译仓库内的 llama.cpp。 |117| `LLAMACPP_ROOT` | 已构建 llama.cpp 的源码/构建根目录;设置后不会编译仓库内的 llama.cpp。 |
116| `MNN_ROOT` | 已构建 MNN 的源码/构建根目录;设置后不会编译仓库内的 MNN。 |118| `MNN_ROOT` | 已构建 MNN 的源码/构建根目录;设置后不会编译仓库内的 MNN。 |
117-| `SMART_SERVE_MODEL_DIR` | 模型权重目录,默认 `$HOME/.cache/models`。 |119+| `SMART_SERVE_MODEL_DIR` | 应用私有的模型下载目录,默认 `$HOME/.cache/models`。生产构建只加载由 SDK 下载并记录为已验证的模型。 |
118| `MODEL_CONFIG_PATH` | `models.json` 的绝对普通文件路径;相对路径和符号链接会被拒绝。非 OpenHarmony 构建不会从源码树、当前目录或安装目录隐式发现配置,应用必须设置该变量,或调用 `GewuSmartServeSetModelsConfigPath()`/`GewuSmartServeSetModelsConfigJson()`。仓库中的 `config/model_config/models.json` 仅作为示例。 |120| `MODEL_CONFIG_PATH` | `models.json` 的绝对普通文件路径;相对路径和符号链接会被拒绝。非 OpenHarmony 构建不会从源码树、当前目录或安装目录隐式发现配置,应用必须设置该变量,或调用 `GewuSmartServeSetModelsConfigPath()`/`GewuSmartServeSetModelsConfigJson()`。仓库中的 `config/model_config/models.json` 仅作为示例。 |
119| `SMART_SERVE_LOG_LEVEL` | 运行时最低日志级别,如 `DEBUG``INFO``WARN`。 |121| `SMART_SERVE_LOG_LEVEL` | 运行时最低日志级别,如 `DEBUG``INFO``WARN`。 |
120| `SMART_SERVE_RUN_NETWORK_TESTS` | 设为 `1` 后运行需要真实网络下载的单元测试。 |122| `SMART_SERVE_RUN_NETWORK_TESTS` | 设为 `1` 后运行需要真实网络下载的单元测试。 |
Minterfaces/innerkits/include/smart_serve_client.h+5-4文件内容审核中,请稍后刷新重试
Minterfaces/innerkits/src/smart_serve_client_non_ohos.cpp+3-3文件内容审核中,请稍后刷新重试
@@ -462,13 +462,18 @@ if [ "${SKIP_CMAKE}" = false ]; then
462 -DCMAKE_BUILD_TYPE="${BUILD_TYPE}"462 -DCMAKE_BUILD_TYPE="${BUILD_TYPE}"
463 -DSMARTSERVE_BUILD_APPLICATIONS="${CMAKE_BUILD_APPLICATIONS}"463 -DSMARTSERVE_BUILD_APPLICATIONS="${CMAKE_BUILD_APPLICATIONS}"
464 -DSMARTSERVE_BUILD_TESTS="${CMAKE_BUILD_TESTS}"464 -DSMARTSERVE_BUILD_TESTS="${CMAKE_BUILD_TESTS}"
465+ -DSMARTSERVE_ENABLE_TEST_FILE_URLS="${CMAKE_BUILD_TESTS}"
465 -DSMARTSERVE_BUILD_SDK="${CMAKE_BUILD_SDK}"466 -DSMARTSERVE_BUILD_SDK="${CMAKE_BUILD_SDK}"
466 )467 )
467 468 
468 if [ "${WITH_MODEL_RUNTIME_TESTS}" = true ]; then469 if [ "${WITH_MODEL_RUNTIME_TESTS}" = true ]; then
469 CMAKE_ARGS+=(-DSMARTSERVE_BUILD_MODEL_RUNTIME_TESTS=ON)470 CMAKE_ARGS+=(-DSMARTSERVE_BUILD_MODEL_RUNTIME_TESTS=ON)
471+ CMAKE_ARGS+=(-DSMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS=ON)
472+ elif [ "${RUN_SMOKE_APPS}" = true ]; then
473+ CMAKE_ARGS+=(-DSMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS=ON)
470 else474 else
471 CMAKE_ARGS+=(-DSMARTSERVE_BUILD_MODEL_RUNTIME_TESTS=OFF)475 CMAKE_ARGS+=(-DSMARTSERVE_BUILD_MODEL_RUNTIME_TESTS=OFF)
476+ CMAKE_ARGS+=(-DSMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS=OFF)
472 fi477 fi
473 478 
474 if [ "${WITH_LLAMACPP}" = true ]; then479 if [ "${WITH_LLAMACPP}" = true ]; then
@@ -48,6 +48,9 @@ target_compile_definitions(smartserve_sdk
48 PRIVATE48 PRIVATE
49 "GEWU_SMARTSERVE_VERSION=\"${SMARTSERVE_SDK_VERSION}\""49 "GEWU_SMARTSERVE_VERSION=\"${SMARTSERVE_SDK_VERSION}\""
50)50)
51+if(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS)
52+ target_compile_definitions(smartserve_sdk PRIVATE SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
53+endif()
51 54 
52set_target_properties(smartserve_sdk PROPERTIES55set_target_properties(smartserve_sdk PROPERTIES
53 OUTPUT_NAME gewu_smartserve56 OUTPUT_NAME gewu_smartserve
@@ -434,7 +434,8 @@ typedef void (*GewuSmartServeDownloadProgressCallback)(
434 *434 *
435 * - Uses models.json configured by the application435 * - Uses models.json configured by the application
436 * - Downloads all required files for the model436 * - Downloads all required files for the model
437- * - Validates checksums437+ * - Requires and validates SHA-256 checksums
hb
hbhb8月18日

interfaces/innerkits/include/smart_serve_client.h:43-53、sdk/include/smartserve/smartserve.h:64

  • SmartServeClient::DownloadModel 保留 checksum = "" 默认参数,但新策略下空 checksum 必然下载失败,默认参数成为「必然失败」陷阱;注释也未提 SHA-256 必填、仅 HTTPS。 老 API SmartServeDownloadModelGewuSmartServeDownloadModel 走同一实现,但只有 gewu_smartserve/core.h 更新了文档。

建议:同步三处公共头注释,并移除 innerkits DownloadModel 的 checksum 默认参数编译期强制调用方提供)。

likedislike
8月18日 评论:
438+ * - Accepts HTTPS download URLs only
438 * - Supports resumable downloads439 * - Supports resumable downloads
439 * - Reports progress via callback440 * - Reports progress via callback
440 *441 *
@@ -466,6 +467,14 @@ typedef void (*GewuSmartServeDownloadProgressCallback)(
466 *467 *
467 * @note This function will block the current thread until download completes.468 * @note This function will block the current thread until download completes.
468 * Consider calling from background thread for large models.469 * Consider calling from background thread for large models.
470+ * @note Every downloaded file must have a valid 64-hex-character SHA-256
471+ * checksum in the model configuration. Missing or malformed checksums
472+ * and non-HTTPS download URLs are rejected with
473+ * GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT.
474+ * @note Production builds load on-disk model artifacts only after this API
475+ * has downloaded them and recorded a verified managed-download entry.
476+ * Copying model files into the models directory, even with matching
477+ * checksums, is not supported in production builds.
469 */478 */
470GewuSmartServeError GewuSmartServeDownloadModel(479GewuSmartServeError GewuSmartServeDownloadModel(
471 const char *modelId, GewuSmartServeDownloadProgressCallback progressCb, void *userData480 const char *modelId, GewuSmartServeDownloadProgressCallback progressCb, void *userData
@@ -518,7 +527,9 @@ GewuSmartServeError GewuSmartServeDeleteModel(const char *modelId);
518 * This is useful for platforms where the default location is not suitable527 * This is useful for platforms where the default location is not suitable
519 * (e.g., Android). Call this before using model management functions.528 * (e.g., Android). Call this before using model management functions.
520 *529 *
521- * @param modelsDir Absolute path to models directory530+ * @param modelsDir Absolute path to the application's private models directory.
531+ * Production callers must not use a directory writable by other principals:
532+ * SmartServe trusts this managed directory after verified downloads complete.
522 * @return Error code (0 = success)533 * @return Error code (0 = success)
523 */534 */
524GewuSmartServeError GewuSmartServeSetModelsDirectory(const char *modelsDir);535GewuSmartServeError GewuSmartServeSetModelsDirectory(const char *modelsDir);
@@ -539,7 +550,7 @@ GewuSmartServeError GewuSmartServeSetModelsDirectory(const char *modelsDir);
539 * "files": [550 * "files": [
540 * {551 * {
541 * "filename": "qwen3-0.6b.mnn",552 * "filename": "qwen3-0.6b.mnn",
542- * "url": "https://...",553+ * "url": "https://models.example.com/model.bin",
543 * "size": 400000000,554 * "size": 400000000,
544 * "checksum": "sha256:..."555 * "checksum": "sha256:..."
545 * }556 * }
@@ -577,7 +588,7 @@ GewuSmartServeError GewuSmartServeSetModelsConfigPath(const char *configPath);
577 * {588 * {
578 * "filename": "llm.mnn",589 * "filename": "llm.mnn",
579 * "size": 450810000,590 * "size": 450810000,
580- * "checksum": ""591+ * "checksum": "d426c65a5159c938ccc237cdfbd982137f276804f27b414ca0ecf3fc0a660f8c"
581 * }592 * }
582 * ]593 * ]
583 * }594 * }
@@ -589,6 +600,8 @@ GewuSmartServeError GewuSmartServeSetModelsConfigPath(const char *configPath);
589 * @return Error code (0 = success)600 * @return Error code (0 = success)
590 *601 *
591 * @note The caller retains ownership of jsonContent and may free it after this call602 * @note The caller retains ownership of jsonContent and may free it after this call
603+ * @note Downloadable files require a 64-hex-character SHA-256 checksum. Model
604+ * download URLs and all redirects must use HTTPS.
592 */605 */
593GewuSmartServeError GewuSmartServeSetModelsConfigJson(const char *jsonContent);606GewuSmartServeError GewuSmartServeSetModelsConfigJson(const char *jsonContent);
594 607 
@@ -61,6 +61,14 @@ typedef void (*SmartServeDownloadProgressCallback)(
61 * finalization calls fail immediately before waiting for the active operation,61 * finalization calls fail immediately before waiting for the active operation,
62 * preventing callback re-entrancy deadlocks. */62 * preventing callback re-entrancy deadlocks. */
63 63 
64+/**
65+ * Downloads the configured model identified by model_id.
66+ *
67+ * Every downloaded file requires a valid 64-hex-character SHA-256 checksum in
68+ * the model configuration, and production downloads accept HTTPS URLs only.
69+ * Missing or malformed checksums and non-HTTPS URLs return
70+ * SMARTSERVE_ERROR_INVALID_ARGUMENT.
71+ */
64SmartServeError SmartServeDownloadModel(72SmartServeError SmartServeDownloadModel(
65 const char* model_id,73 const char* model_id,
66 SmartServeDownloadProgressCallback progress_cb,74 SmartServeDownloadProgressCallback progress_cb,
@@ -31,6 +31,7 @@
31#include "api.h"31#include "api.h"
32#include "cabi_engine_creator.h"32#include "cabi_engine_creator.h"
33#include "engine_abi/engine.h"33#include "engine_abi/engine.h"
34+#include "model/model_config.h"
34#include "nlohmann/json.hpp"35#include "nlohmann/json.hpp"
35#include "service/service.h"36#include "service/service.h"
36#include "smart_serve_log.h"37#include "smart_serve_log.h"
@@ -535,7 +536,7 @@ GewuSmartServeError GewuSmartServeGetModelInfo(const char* modelId, char** model
535GewuSmartServeError GewuSmartServeUnloadModel(const char* modelId)536GewuSmartServeError GewuSmartServeUnloadModel(const char* modelId)
536{537{
537 GEWU_API_GUARD();538 GEWU_API_GUARD();
538- if (IsNullOrEmpty(modelId)) {539+ if (IsNullOrEmpty(modelId) || !OHOS::SmartServe::IsValidModelId(modelId)) {
539 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;540 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
540 }541 }
541 const GewuSmartServeError initErr = SmartServeInternal::Initialize();542 const GewuSmartServeError initErr = SmartServeInternal::Initialize();
@@ -43,6 +43,8 @@ using OHOS::SmartServe::ModelStorageSubPath;
43using OHOS::SmartServe::NormalizeChecksum;43using OHOS::SmartServe::NormalizeChecksum;
44using OHOS::SmartServe::ResolveModelFilePath;44using OHOS::SmartServe::ResolveModelFilePath;
45using OHOS::SmartServe::ResolveModelStoragePath;45using OHOS::SmartServe::ResolveModelStoragePath;
46+using OHOS::SmartServe::ResolveSafeDeletionPath;
47+using OHOS::SmartServe::RemoveManagedPathNoFollow;
46 48 
47thread_local size_t g_downloadProgressCallbackDepth = 0;49thread_local size_t g_downloadProgressCallbackDepth = 0;
48 50 
@@ -259,25 +261,37 @@ Json BuildModelPathDiagnostics(
259 return diagnostics;261 return diagnostics;
260}262}
261 263 
264+OHOS::SmartServe::DownloadError SaveManagedModelMetadata(
265+ const std::string& modelId,
266+ const OHOS::SmartServe::ModelConfig& config,
267+ const std::string& modelPath,
268+ const std::string& metadataPath,
269+ const std::string& status,
270+ bool verified)
271+{
272+ auto& manager = OHOS::SmartServe::ModelFileManager::Instance();
273+ OHOS::SmartServe::ModelFileManager::InstalledModelInfo info;
274+ info.status = status;
275+ info.path = modelPath;
276+ info.sizeBytes = ConfiguredModelSize(config);
277+ if (verified && info.sizeBytes == 0) {
278+ info.sizeBytes = CalculatePathSize(modelPath);
279+ }
280+ info.checksum = NormalizeChecksum(config.checksum);
281+ info.engine = config.engine;
282+ info.created = CurrentUnixTimestamp();
283+ info.verified = verified;
284+ return manager.SaveInstalledModelInfo(modelId, info, metadataPath);
285+}
286+ 
262void SaveDownloadedMetadata(287void SaveDownloadedMetadata(
263 const std::string& modelId,288 const std::string& modelId,
264 const OHOS::SmartServe::ModelConfig& config,289 const OHOS::SmartServe::ModelConfig& config,
265 const std::string& modelPath,290 const std::string& modelPath,
266 const std::string& metadataPath)291 const std::string& metadataPath)
267{292{
268- auto& manager = OHOS::SmartServe::ModelFileManager::Instance();293+ const auto error = SaveManagedModelMetadata(
269- OHOS::SmartServe::ModelFileManager::InstalledModelInfo info;294+ modelId, config, modelPath, metadataPath, "downloaded", true);
270- info.status = "downloaded";
271- info.path = modelPath;
272- info.sizeBytes = ConfiguredModelSize(config);
273- if (info.sizeBytes == 0) {
274- info.sizeBytes = CalculatePathSize(modelPath);
275- }
276- info.checksum = NormalizeChecksum(config.checksum);
277- info.engine = config.engine;
278- info.created = CurrentUnixTimestamp();
279- info.verified = true;
280- const auto error = manager.SaveInstalledModelInfo(modelId, info, metadataPath);
281 if (error != OHOS::SmartServe::DownloadError::OK) {295 if (error != OHOS::SmartServe::DownloadError::OK) {
282 SMART_SERVE_LOGW("[SmartServeModelMetadata] failed to save metadata model=%s err=%d",296 SMART_SERVE_LOGW("[SmartServeModelMetadata] failed to save metadata model=%s err=%d",
283 modelId.c_str(), static_cast<int>(error));297 modelId.c_str(), static_cast<int>(error));
@@ -303,29 +317,23 @@ std::string ModelDownloadStatusForStableSnapshot(
303 if (basePath.empty()) {317 if (basePath.empty()) {
304 return "invalid";318 return "invalid";
305 }319 }
306- const std::string metadataPath = InstalledMetadataPath(modelDir);
307- auto& fileManager = OHOS::SmartServe::ModelFileManager::Instance();
308- const auto installedInfo = fileManager.GetInstalledModelInfo(config.id, metadataPath);
309- 
310 if (!config.files.empty()) {320 if (!config.files.empty()) {
311 bool any = false;321 bool any = false;
312 bool all = true;322 bool all = true;
313 for (const auto& file : config.files) {323 for (const auto& file : config.files) {
314 std::string destination;324 std::string destination;
325+ const std::string checksum = NormalizeChecksum(file.checksum);
315 if (!ResolveModelFilePath(basePath, file.filename, destination)) {326 if (!ResolveModelFilePath(basePath, file.filename, destination)) {
316 return "invalid";327 return "invalid";
317 }328 }
318 if (std::filesystem::exists(destination)) {329 if (std::filesystem::exists(destination)) {
319 any = true;330 any = true;
320 }331 }
321- if (!client.IsModelReady(destination, NormalizeChecksum(file.checksum))) {332+ if (!OHOS::SmartServe::IsValidSha256Checksum(checksum) || !client.IsModelReady(destination, checksum)) {
322 all = false;333 all = false;
323 }334 }
324 }335 }
325 if (all && any) {336 if (all && any) {
326- if (!installedInfo) {
327- SaveDownloadedMetadata(config.id, config, basePath, metadataPath);
328- }
329 return "downloaded";337 return "downloaded";
330 }338 }
331 if (any) {339 if (any) {
@@ -334,11 +342,10 @@ std::string ModelDownloadStatusForStableSnapshot(
334 return "configured";342 return "configured";
335 }343 }
336 if (OHOS::SmartServe::UsesSingleFileStoragePath(config)) {344 if (OHOS::SmartServe::UsesSingleFileStoragePath(config)) {
345+ const std::string checksum = NormalizeChecksum(config.checksum);
337 if (std::filesystem::is_regular_file(basePath)) {346 if (std::filesystem::is_regular_file(basePath)) {
338- if (client.IsModelReady(basePath, NormalizeChecksum(config.checksum))) {347+ if (OHOS::SmartServe::IsValidSha256Checksum(checksum) &&
339- if (!installedInfo) {348+ client.IsModelReady(basePath, checksum)) {
hb
hbhb8月18日

model_lifecycle_service.cpp:354-358 分支不可达(前面 310/337 行已穷尽 base_url 非空情形),属死代码

likedislike
8月18日 评论:
340- SaveDownloadedMetadata(config.id, config, basePath, metadataPath);
341- }
342 return "downloaded";349 return "downloaded";
343 }350 }
344 return "partial";351 return "partial";
@@ -348,12 +355,6 @@ std::string ModelDownloadStatusForStableSnapshot(
348 }355 }
349 return "configured";356 return "configured";
350 }357 }
351- if (!config.base_url.empty() && std::filesystem::exists(basePath)) {
352- if (!installedInfo) {
353- SaveDownloadedMetadata(config.id, config, basePath, metadataPath);
354- }
355- return "downloaded";
356- }
357 if (std::filesystem::exists(basePath)) {358 if (std::filesystem::exists(basePath)) {
358 return "partial";359 return "partial";
359 }360 }
@@ -393,6 +394,23 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
393 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;394 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
394 }395 }
395 const std::string metadataPath = InstalledMetadataPath(modelDir);396 const std::string metadataPath = InstalledMetadataPath(modelDir);
397+ std::string safeFilePath;
398+ if (!ResolveSafeDeletionPath(modelDir, filePath, safeFilePath)) {
399+ SMART_SERVE_LOGE("[SmartServeDownloadModel] model path escapes model root id=%s path=%s",
400+ modelId.c_str(), filePath.c_str());
401+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
402+ }
403+ filePath = std::move(safeFilePath);
404+ auto& fileManager = OHOS::SmartServe::ModelFileManager::Instance();
405+ const auto installedInfo = fileManager.GetInstalledModelInfo(modelId, metadataPath);
406+ if (installedInfo) {
407+ std::string registeredPath;
408+ if (!ResolveSafeDeletionPath(modelDir, installedInfo->path, registeredPath) ||
409+ registeredPath != filePath) {
410+ SMART_SERVE_LOGE("[SmartServeDownloadModel] registered path mismatch id=%s", modelId.c_str());
411+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
412+ }
413+ }
396 414 
397 if (!config.files.empty()) {415 if (!config.files.empty()) {
398 int64_t totalBytes = 0;416 int64_t totalBytes = 0;
@@ -406,12 +424,22 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
406 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;424 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
407 }425 }
408 }426 }
409- std::filesystem::create_directories(filePath);
410 int64_t doneBytes = 0;427 int64_t doneBytes = 0;
411 std::vector<OHOS::SmartServe::ModelFileManager::FileDownloadSpec> pendingFiles;428 std::vector<OHOS::SmartServe::ModelFileManager::FileDownloadSpec> pendingFiles;
412 for (const auto& file : config.files) {429 for (const auto& file : config.files) {
413 const std::string fileUrl = !file.url.empty()430 const std::string fileUrl = !file.url.empty()
414 ? file.url : (config.base_url + "/" + file.filename);431 ? file.url : (config.base_url + "/" + file.filename);
432+ if (!OHOS::SmartServe::IsAllowedModelDownloadSource(fileUrl)) {
433+ SMART_SERVE_LOGE("[SmartServeDownloadModel] insecure download source id=%s url=%s",
hb
hbhb8月18日

sdk/src/model_lifecycle_service.cpp:419,498、services/src/server/model/model_file_manager.cpp:425、model_downloader.cpp:436

URL 被原样写入日志,可能泄露凭据。

新增的拒绝日志会将带 userinfo 的非法 URL(如 https://user:password@host/x)完整写入日志,直接泄露账号密码,违反项目日志规范。

此外既有下载失败日志也会记录合法 URL 中的签名 query。

建议提供统一的 URL 脱敏函数,仅保留 scheme、host、port 和必要的路径信息,并移除 userinfo、query、fragment。

likedislike
8月18日 评论:
434+ modelId.c_str(), OHOS::SmartServe::RedactUrlForLog(fileUrl).c_str());
435+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
436+ }
437+ const std::string checksum = NormalizeChecksum(file.checksum);
438+ if (!OHOS::SmartServe::IsValidSha256Checksum(checksum)) {
439+ SMART_SERVE_LOGE("[SmartServeDownloadModel] missing or invalid SHA-256 id=%s file=%s",
440+ modelId.c_str(), file.filename.c_str());
441+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
442+ }
415 std::string destination;443 std::string destination;
416 if (!ResolveModelFilePath(filePath, file.filename, destination)) {444 if (!ResolveModelFilePath(filePath, file.filename, destination)) {
417 SMART_SERVE_LOGE("[SmartServeDownloadModel] unsafe model file path id=%s file=%s",445 SMART_SERVE_LOGE("[SmartServeDownloadModel] unsafe model file path id=%s file=%s",
@@ -419,7 +447,7 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
419 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;447 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
420 }448 }
421 if (std::filesystem::exists(destination) &&449 if (std::filesystem::exists(destination) &&
422- client.IsModelReady(destination, NormalizeChecksum(file.checksum))) {450+ client.IsModelReady(destination, checksum)) {
423 const int64_t fileSize = file.size > 0 ? file.size : 0;451 const int64_t fileSize = file.size > 0 ? file.size : 0;
424 if (!CheckedAddNonNegative(doneBytes, fileSize, doneBytes)) {452 if (!CheckedAddNonNegative(doneBytes, fileSize, doneBytes)) {
425 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;453 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
@@ -435,14 +463,26 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
435 spec.url = fileUrl;463 spec.url = fileUrl;
436 spec.destinationPath = destination;464 spec.destinationPath = destination;
437 spec.expectedSize = file.size;465 spec.expectedSize = file.size;
438- spec.checksum = NormalizeChecksum(file.checksum);466+ spec.checksum = checksum;
439 pendingFiles.push_back(std::move(spec));467 pendingFiles.push_back(std::move(spec));
440 }468 }
441 if (pendingFiles.empty()) {469 if (pendingFiles.empty()) {
442- SaveDownloadedMetadata(modelId, config, filePath, metadataPath);470+ if (installedInfo) {
471+ SaveDownloadedMetadata(modelId, config, filePath, metadataPath);
472+ }
443 return GEWU_SMARTSERVE_OK;473 return GEWU_SMARTSERVE_OK;
444 }474 }
445- const auto error = OHOS::SmartServe::ModelFileManager::Instance().DownloadModelFiles(475+ if (!installedInfo && std::filesystem::exists(filePath)) {
476+ SMART_SERVE_LOGE("[SmartServeDownloadModel] refusing to claim unmanaged path id=%s path=%s",
477+ modelId.c_str(), filePath.c_str());
478+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
479+ }
480+ const auto registrationError = SaveManagedModelMetadata(
481+ modelId, config, filePath, metadataPath, "partial", false);
482+ if (registrationError != OHOS::SmartServe::DownloadError::OK) {
483+ return MapDownloadError(registrationError);
484+ }
485+ const auto error = fileManager.DownloadModelFiles(
446 modelId, pendingFiles,486 modelId, pendingFiles,
447 [&](const std::string&, int64_t downloaded, int64_t total, float) {487 [&](const std::string&, int64_t downloaded, int64_t total, float) {
448 const int64_t aggregateDownloaded =488 const int64_t aggregateDownloaded =
@@ -457,7 +497,7 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
457 progressCallback, aggregateDownloaded, aggregateTotal, aggregateProgress);497 progressCallback, aggregateDownloaded, aggregateTotal, aggregateProgress);
458 },498 },
459 true,499 true,
460- filePath);500+ modelDir);
461 const GewuSmartServeError mappedError = MapDownloadError(error);501 const GewuSmartServeError mappedError = MapDownloadError(error);
462 if (mappedError == GEWU_SMARTSERVE_ERROR_CANCELLED) {502 if (mappedError == GEWU_SMARTSERVE_ERROR_CANCELLED) {
463 SMART_SERVE_LOGI("[SmartServeDownloadModel] paused model=%s", modelId.c_str());503 SMART_SERVE_LOGI("[SmartServeDownloadModel] paused model=%s", modelId.c_str());
@@ -470,39 +510,56 @@ GewuSmartServeError ModelLifecycleService::DownloadModel(
470 return mappedError;510 return mappedError;
471 }511 }
472 512 
473- if (!config.base_url.empty()) {513+ if (config.base_url.empty()) {
474- const auto parent = std::filesystem::path(filePath).parent_path();
475- if (!parent.empty()) {
476- std::filesystem::create_directories(parent);
477- }
478- } else {
479 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;514 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
480 }515 }
481 516 
482- if (client.IsModelReady(filePath, NormalizeChecksum(config.checksum))) {517+ const std::string checksum = NormalizeChecksum(config.checksum);
518+ if (!OHOS::SmartServe::IsAllowedModelDownloadSource(config.base_url)) {
519+ SMART_SERVE_LOGE("[SmartServeDownloadModel] insecure download source id=%s url=%s",
520+ modelId.c_str(), OHOS::SmartServe::RedactUrlForLog(config.base_url).c_str());
521+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
522+ }
523+ if (!OHOS::SmartServe::IsValidSha256Checksum(checksum)) {
524+ SMART_SERVE_LOGE("[SmartServeDownloadModel] missing or invalid SHA-256 id=%s", modelId.c_str());
525+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
526+ }
527+ if (client.IsModelReady(filePath, checksum)) {
483 InvokeDownloadProgress(progressCallback, 1, 1, 1.0f);528 InvokeDownloadProgress(progressCallback, 1, 1, 1.0f);
484- SaveDownloadedMetadata(modelId, config, filePath, metadataPath);529+ if (installedInfo) {
530+ SaveDownloadedMetadata(modelId, config, filePath, metadataPath);
531+ }
485 return GEWU_SMARTSERVE_OK;532 return GEWU_SMARTSERVE_OK;
486 }533 }
534+ if (!installedInfo && std::filesystem::exists(filePath)) {
535+ SMART_SERVE_LOGE("[SmartServeDownloadModel] refusing to claim unmanaged path id=%s path=%s",
536+ modelId.c_str(), filePath.c_str());
537+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
538+ }
539+ const auto registrationError = SaveManagedModelMetadata(
540+ modelId, config, filePath, metadataPath, "partial", false);
541+ if (registrationError != OHOS::SmartServe::DownloadError::OK) {
542+ return MapDownloadError(registrationError);
543+ }
487 OHOS::SmartServe::ModelFileManager::FileDownloadSpec spec;544 OHOS::SmartServe::ModelFileManager::FileDownloadSpec spec;
488 spec.url = config.base_url;545 spec.url = config.base_url;
489 spec.destinationPath = filePath;546 spec.destinationPath = filePath;
490 spec.expectedSize = config.size;547 spec.expectedSize = config.size;
491- spec.checksum = NormalizeChecksum(config.checksum);548+ spec.checksum = checksum;
492- const auto error = OHOS::SmartServe::ModelFileManager::Instance().DownloadModelFiles(549+ const auto error = fileManager.DownloadModelFiles(
493 modelId, {spec},550 modelId, {spec},
494 [&](const std::string&, int64_t downloaded, int64_t total, float progress) {551 [&](const std::string&, int64_t downloaded, int64_t total, float progress) {
495 InvokeDownloadProgress(progressCallback, downloaded, total, progress);552 InvokeDownloadProgress(progressCallback, downloaded, total, progress);
496 },553 },
497 true,554 true,
498- filePath);555+ modelDir);
499 const GewuSmartServeError mappedError = MapDownloadError(error);556 const GewuSmartServeError mappedError = MapDownloadError(error);
500 if (mappedError == GEWU_SMARTSERVE_ERROR_CANCELLED) {557 if (mappedError == GEWU_SMARTSERVE_ERROR_CANCELLED) {
501 SMART_SERVE_LOGI("[SmartServeDownloadModel] paused model=%s dest=%s",558 SMART_SERVE_LOGI("[SmartServeDownloadModel] paused model=%s dest=%s",
502 modelId.c_str(), filePath.c_str());559 modelId.c_str(), filePath.c_str());
503 } else if (mappedError != GEWU_SMARTSERVE_OK) {560 } else if (mappedError != GEWU_SMARTSERVE_OK) {
504 SMART_SERVE_LOGE("[SmartServeDownloadModel] single-file failed id=%s url=%s dest=%s",561 SMART_SERVE_LOGE("[SmartServeDownloadModel] single-file failed id=%s url=%s dest=%s",
505- modelId.c_str(), config.base_url.c_str(), filePath.c_str());562+ modelId.c_str(), OHOS::SmartServe::RedactUrlForLog(config.base_url).c_str(), filePath.c_str());
506 } else {563 } else {
507 SaveDownloadedMetadata(modelId, config, filePath, metadataPath);564 SaveDownloadedMetadata(modelId, config, filePath, metadataPath);
508 }565 }
@@ -553,6 +610,12 @@ GewuSmartServeError ModelLifecycleService::DeleteModel(
553 }610 }
554 }611 }
555 }612 }
613+ std::string safePath;
614+ if (!ResolveSafeDeletionPath(modelDir, path, safePath)) {
615+ SMART_SERVE_LOGE("[SmartServeDeleteModel] model path escapes model root id=%s path=%s",
616+ modelId.c_str(), path.c_str());
617+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
618+ }
556 auto modelFileOperation =619 auto modelFileOperation =
557 OHOS::SmartServe::ModelFileOperationCoordinator::Instance().AcquireDelete(modelId);620 OHOS::SmartServe::ModelFileOperationCoordinator::Instance().AcquireDelete(modelId);
558 const std::string metadataPath = InstalledMetadataPath(modelDir);621 const std::string metadataPath = InstalledMetadataPath(modelDir);
@@ -579,6 +642,19 @@ GewuSmartServeError ModelLifecycleService::DeleteModel(
579 } unblock{&service->apiHandler_, modelId};642 } unblock{&service->apiHandler_, modelId};
580 643 
581 auto& fileManager = OHOS::SmartServe::ModelFileManager::Instance();644 auto& fileManager = OHOS::SmartServe::ModelFileManager::Instance();
645+ const auto installedInfo = fileManager.GetInstalledModelInfo(modelId, metadataPath);
646+ if (installedInfo) {
647+ std::string registeredPath;
648+ if (!ResolveSafeDeletionPath(modelDir, installedInfo->path, registeredPath) ||
649+ registeredPath != safePath) {
650+ SMART_SERVE_LOGE("[SmartServeDeleteModel] registered path mismatch id=%s", modelId.c_str());
651+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
652+ }
653+ } else if (std::filesystem::exists(safePath)) {
654+ SMART_SERVE_LOGE("[SmartServeDeleteModel] refusing to delete unmanaged path id=%s path=%s",
655+ modelId.c_str(), safePath.c_str());
656+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
657+ }
582 const auto cancelError = fileManager.CancelDownload(modelId);658 const auto cancelError = fileManager.CancelDownload(modelId);
583 if (cancelError != OHOS::SmartServe::DownloadError::OK) {659 if (cancelError != OHOS::SmartServe::DownloadError::OK) {
584 return MapDownloadError(cancelError);660 return MapDownloadError(cancelError);
@@ -589,8 +665,8 @@ GewuSmartServeError ModelLifecycleService::DeleteModel(
589 }665 }
590 modelFileOperation.WaitForActiveOperationsToDrain();666 modelFileOperation.WaitForActiveOperationsToDrain();
591 667 
592- const bool hasFilesOnDisk = std::filesystem::exists(path);668+ const bool hasFilesOnDisk = std::filesystem::exists(safePath);
593- const bool hasMetadata = fileManager.GetInstalledModelInfo(modelId, metadataPath).has_value();669+ const bool hasMetadata = installedInfo.has_value();
594 bool wasLoaded = false;670 bool wasLoaded = false;
595 const GewuSmartServeError unloadError =671 const GewuSmartServeError unloadError =
596 UnloadLoadedModel(service.Get(), modelId, wasLoaded);672 UnloadLoadedModel(service.Get(), modelId, wasLoaded);
@@ -610,13 +686,21 @@ GewuSmartServeError ModelLifecycleService::DeleteModel(
610 modelId.c_str(), activeAfterUnload);686 modelId.c_str(), activeAfterUnload);
611 return GEWU_SMARTSERVE_ERROR_ENGINE_BUSY;687 return GEWU_SMARTSERVE_ERROR_ENGINE_BUSY;
612 }688 }
613- if (std::filesystem::exists(path)) {689+ std::string finalSafePath;
614- std::filesystem::remove_all(path);690+ if (!ResolveSafeDeletionPath(modelDir, path, finalSafePath) || finalSafePath != safePath) {
691+ SMART_SERVE_LOGE("[SmartServeDeleteModel] model path changed before delete id=%s", modelId.c_str());
692+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
615 }693 }
616- const auto metadataError = fileManager.RemoveInstalledModelInfo(modelId, metadataPath);694+ if (!RemoveManagedPathNoFollow(modelDir, path)) {
617- if (metadataError != OHOS::SmartServe::DownloadError::OK) {695+ SMART_SERVE_LOGE("[SmartServeDeleteModel] secure model removal failed id=%s", modelId.c_str());
618- SMART_SERVE_LOGW("[SmartServeDeleteModel] failed to remove metadata id=%s err=%d",696+ return GEWU_SMARTSERVE_ERROR_FILE_WRITE;
619- modelId.c_str(), static_cast<int>(metadataError));697+ }
698+ if (installedInfo) {
699+ const auto metadataError = fileManager.RemoveInstalledModelInfo(modelId, metadataPath);
700+ if (metadataError != OHOS::SmartServe::DownloadError::OK) {
701+ SMART_SERVE_LOGW("[SmartServeDeleteModel] failed to remove metadata id=%s err=%d",
702+ modelId.c_str(), static_cast<int>(metadataError));
703+ }
620 }704 }
621 return GEWU_SMARTSERVE_OK;705 return GEWU_SMARTSERVE_OK;
622 } catch (...) {706 } catch (...) {
@@ -87,6 +87,21 @@ bool IsNullOrEmpty(const char* value)
87 return value == nullptr || value[0] == '\0';87 return value == nullptr || value[0] == '\0';
88}88}
89 89 
90+bool IsPrivateModelsDirectory(const std::filesystem::path& path)
91+{
92+#if defined(_WIN32) || defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
93+ (void)path;
94+ return true;
95+#else
96+ struct stat info {};
97+ if (stat(path.c_str(), &info) != 0 || !S_ISDIR(info.st_mode)) {
98+ return false;
99+ }
100+ constexpr mode_t kUntrustedWritePermissions = S_IWGRP | S_IWOTH;
101+ return info.st_uid == geteuid() && (info.st_mode & kUntrustedWritePermissions) == 0;
102+#endif
103+}
104+ 
90GewuSmartServeError ValidateModelsConfigFile(const char* path)105GewuSmartServeError ValidateModelsConfigFile(const char* path)
91{106{
92 try {107 try {
@@ -384,7 +399,16 @@ GewuSmartServeError SetEnvModelDir(const char* dir)
384 } else {399 } else {
385 std::filesystem::create_directories(modelDir);400 std::filesystem::create_directories(modelDir);
386 }401 }
387- resolvedDir = std::filesystem::canonical(modelDir).string();402+ const std::filesystem::path canonicalModelDir = std::filesystem::canonical(modelDir);
403+ if (!std::filesystem::is_directory(canonicalModelDir)) {
404+ SMART_SERVE_LOGE("[SetEnvModelDir] canonical model path is not a directory: %s", dir);
405+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
406+ }
407+ if (!IsPrivateModelsDirectory(canonicalModelDir)) {
408+ SMART_SERVE_LOGE("[SetEnvModelDir] model directory is not private: %s", dir);
409+ return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
410+ }
411+ resolvedDir = canonicalModelDir.string();
388 } catch (const std::exception& e) {412 } catch (const std::exception& e) {
389 SMART_SERVE_LOGE("[SetEnvModelDir] failed to create model dir: %s (%s)",413 SMART_SERVE_LOGE("[SetEnvModelDir] failed to create model dir: %s (%s)",
390 dir, e.what());414 dir, e.what());
@@ -486,7 +510,7 @@ GewuSmartServeError DownloadModel(
486 void* user_data) noexcept510 void* user_data) noexcept
487{511{
488 try {512 try {
489- if (!model_id || model_id[0] == '\0') {513+ if (!model_id || !OHOS::SmartServe::IsValidModelId(model_id)) {
490 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;514 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
491 }515 }
492 const GewuSmartServeError initErr = Initialize();516 const GewuSmartServeError initErr = Initialize();
@@ -540,7 +564,7 @@ GewuSmartServeError DownloadModel(
540 564 
541GewuSmartServeError PauseDownload(const char* model_id)565GewuSmartServeError PauseDownload(const char* model_id)
542{566{
543- if (!model_id || model_id[0] == '\0') {567+ if (!model_id || !OHOS::SmartServe::IsValidModelId(model_id)) {
544 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;568 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
545 }569 }
546 return ModelLifecycleService::Instance().PauseDownload(model_id);570 return ModelLifecycleService::Instance().PauseDownload(model_id);
@@ -549,7 +573,7 @@ GewuSmartServeError PauseDownload(const char* model_id)
549GewuSmartServeError DeleteModel(const char* model_id) noexcept573GewuSmartServeError DeleteModel(const char* model_id) noexcept
550{574{
551 try {575 try {
552- if (!model_id || model_id[0] == '\0') {576+ if (!model_id || !OHOS::SmartServe::IsValidModelId(model_id)) {
553 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;577 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
554 }578 }
555 const GewuSmartServeError initError = Initialize();579 const GewuSmartServeError initError = Initialize();
@@ -719,7 +743,7 @@ GewuSmartServeError ListModels(char** out_json) noexcept
719GewuSmartServeError GetModelInfo(const char* model_id, char** out_json) noexcept743GewuSmartServeError GetModelInfo(const char* model_id, char** out_json) noexcept
720{744{
721 try {745 try {
722- if (!model_id || model_id[0] == '\0' || !out_json) {746+ if (!model_id || !OHOS::SmartServe::IsValidModelId(model_id) || !out_json) {
723 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;747 return GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT;
724 }748 }
725 *out_json = nullptr;749 *out_json = nullptr;
Mservices/src/server/BUILD.gn+1-0文件内容审核中,请稍后刷新重试
@@ -19,6 +19,7 @@
19 19 
20#include "assertion.h"20#include "assertion.h"
21#include "model/model_downloader.h"21#include "model/model_downloader.h"
22+#include "model/model_util.h"
22#include "service/service.h"23#include "service/service.h"
23#include "smart_serve_log.h"24#include "smart_serve_log.h"
24 25 
@@ -40,8 +41,36 @@ bool VerifyModelFile(const std::string& modelId, const std::filesystem::path& pa
40 return true;41 return true;
41}42}
42 43 
44+bool RejectUnverifiedPreinstalledModel(
45+ const std::string& modelId, const std::filesystem::path& path, const char* reason)
46+{
47+ SMART_SERVE_LOGE(TAG "Refusing unverified preinstalled model: model=%s file=%s reason=%s",
48+ modelId.c_str(), path.string().c_str(), reason);
49+ return false;
50+}
51+ 
52+#if !defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
53+bool HasOnDiskModelArtifact(const ModelConfig& config)
54+{
55+ if (!config.files.empty() || !config.base_url.empty()) {
56+ return true;
57+ }
58+ if (config.name.empty()) {
59+ return false;
60+ }
61+ std::error_code error;
62+ return std::filesystem::exists(config.name, error) && !error;
63+}
64+#endif
65+ 
43bool VerifyModelFilesBeforeLoad(const ModelConfig& config)66bool VerifyModelFilesBeforeLoad(const ModelConfig& config)
44{67{
68+#if !defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
69+ if (!config.managed_download_verified && HasOnDiskModelArtifact(config)) {
70+ SMART_SERVE_LOGE(TAG "Refusing model without a verified managed download: %s", config.id.c_str());
71+ return false;
72+ }
73+#endif
45 if (!config.files.empty()) {74 if (!config.files.empty()) {
46 const std::filesystem::path modelDir(config.name);75 const std::filesystem::path modelDir(config.name);
47 for (const auto& file : config.files) {76 for (const auto& file : config.files) {
@@ -52,23 +81,43 @@ bool VerifyModelFilesBeforeLoad(const ModelConfig& config)
52 config.id.c_str(), filePath.string().c_str());81 config.id.c_str(), filePath.string().c_str());
53 return false;82 return false;
54 }83 }
55- if (file.checksum.empty()) {84+ const std::string checksum = NormalizeChecksum(file.checksum);
56- SMART_SERVE_LOGW(TAG "Skip checksum verification for model=%s file=%s: checksum empty",85+ if (!IsValidSha256Checksum(checksum)) {
57- config.id.c_str(), file.filename.c_str());86+ return RejectUnverifiedPreinstalledModel(config.id, filePath, "missing or invalid SHA-256");
58- continue;
59 }87 }
60- if (!VerifyModelFile(config.id, filePath, file.checksum)) {88+ if (!VerifyModelFile(config.id, filePath, checksum)) {
61 return false;89 return false;
62 }90 }
63 }91 }
64 return true;92 return true;
65 }93 }
66 94 
67- if (!config.checksum.empty()) {95+ const std::filesystem::path modelPath(config.name);
68- return VerifyModelFile(config.id, std::filesystem::path(config.name), config.checksum);96+ std::error_code error;
97+ if (!std::filesystem::exists(modelPath, error)) {
98+ if (error) {
99+ SMART_SERVE_LOGE(TAG "Cannot inspect model path before load: model=%s file=%s",
100+ config.id.c_str(), modelPath.string().c_str());
101+ return false;
102+ }
103+ // Configurations without on-disk artifacts are implemented by the engine
104+ // (for example, a system-provided model) and have no file to verify here.
105+ return true;
106+ }
107+ if (!std::filesystem::is_regular_file(modelPath, error)) {
108+ if (error) {
109+ SMART_SERVE_LOGE(TAG "Cannot inspect model path before load: model=%s file=%s",
110+ config.id.c_str(), modelPath.string().c_str());
111+ return false;
112+ }
113+ return RejectUnverifiedPreinstalledModel(config.id, modelPath, "no file checksum manifest");
69 }114 }
70 115 
71- return true;116+ const std::string checksum = NormalizeChecksum(config.checksum);
117+ if (!IsValidSha256Checksum(checksum)) {
118+ return RejectUnverifiedPreinstalledModel(config.id, modelPath, "missing or invalid SHA-256");
119+ }
120+ return VerifyModelFile(config.id, modelPath, checksum);
72}121}
73 122 
74} // namespace123} // namespace
@@ -110,13 +159,13 @@ Model* ModelManager::GetOrCreateModel(const std::string& name)
110 }159 }
111 ma = engine->CreateModel(*config);160 ma = engine->CreateModel(*config);
112 } else {161 } else {
113- // Legacy path: plugins may still register one ModelCreator per model.162+ // Legacy creators do not provide an integrity manifest.
114- auto mc = GetCreator(name);163+ if (GetCreator(name) == nullptr) {
115- if (mc == nullptr) {
116 SMART_SERVE_LOGE(TAG "Model config not found: %s", name.c_str());164 SMART_SERVE_LOGE(TAG "Model config not found: %s", name.c_str());
117 return nullptr;165 return nullptr;
118 }166 }
119- ma = mc->Create();167+ SMART_SERVE_LOGE(TAG "Refusing legacy model without an integrity manifest: %s", name.c_str());
168+ return nullptr;
120 }169 }
121 170 
122 if (ma == nullptr) {171 if (ma == nullptr) {
Mservices/src/server/model/model_config.h+20-1文件内容审核中,请稍后刷新重试
@@ -23,6 +23,11 @@
23#include <limits>23#include <limits>
24#include <sys/stat.h>24#include <sys/stat.h>
25 25 
26+#if !defined(_WIN32)
27+#include <fcntl.h>
28+#include <unistd.h>
29+#endif
30+ 
26#include "model/model_downloader.h"31#include "model/model_downloader.h"
27#include "model/model_util.h"32#include "model/model_util.h"
28#include "smart_serve_log.h"33#include "smart_serve_log.h"
@@ -118,6 +123,105 @@ DownloadError RemoveExistingFile(const std::string& filePath)
118 return DownloadError::OK;123 return DownloadError::OK;
119}124}
120 125 
126+#if !defined(_WIN32)
127+int64_t GetOpenFileSize(int fd)
128+{
129+ struct stat st;
130+ if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
131+ return -1;
132+ }
133+ return st.st_size;
134+}
135+ 
136+DownloadError ResetOpenFile(int fd)
137+{
138+ if (ftruncate(fd, 0) != 0 || lseek(fd, 0, SEEK_SET) < 0) {
139+ return DownloadError::FILE_ERROR;
140+ }
141+ return DownloadError::OK;
142+}
143+ 
144+DownloadError MakeOpenFileReadOnly(int fd)
145+{
146+ struct stat st;
147+ if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
148+ return DownloadError::FILE_ERROR;
149+ }
150+ if (fchmod(fd, st.st_mode & ~(S_IWUSR | S_IWGRP | S_IWOTH)) != 0) {
151+ return DownloadError::FILE_ERROR;
152+ }
153+ return DownloadError::OK;
154+}
155+ 
156+std::string CalculateSHA256FromOpenFile(int fd)
157+{
158+ const int readFd = dup(fd);
159+ if (readFd < 0 || lseek(readFd, 0, SEEK_SET) < 0) {
160+ if (readFd >= 0) {
161+ close(readFd);
162+ }
163+ throw std::runtime_error("Failed to duplicate file for checksum calculation");
164+ }
165+ 
166+ char buffer[16 * 1024];
167+#if defined(__ANDROID__) || defined(__linux__)
168+ EVP_MD_CTX* ctx = EVP_MD_CTX_new();
169+ if (ctx == nullptr || EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1) {
170+ if (ctx != nullptr) {
171+ EVP_MD_CTX_free(ctx);
172+ }
173+ close(readFd);
174+ throw std::runtime_error("Failed to initialize SHA256");
175+ }
176+ ssize_t count = 0;
177+ while ((count = read(readFd, buffer, sizeof(buffer))) > 0) {
178+ if (EVP_DigestUpdate(ctx, buffer, static_cast<size_t>(count)) != 1) {
179+ EVP_MD_CTX_free(ctx);
180+ close(readFd);
181+ throw std::runtime_error("Failed to update SHA256");
182+ }
183+ }
184+ if (count < 0) {
185+ EVP_MD_CTX_free(ctx);
186+ close(readFd);
187+ throw std::runtime_error("Failed to read file for checksum calculation");
188+ }
189+ unsigned char hash[SHA256_DIGEST_LENGTH];
190+ unsigned int hashLength = 0;
191+ const int finalized = EVP_DigestFinal_ex(ctx, hash, &hashLength);
192+ EVP_MD_CTX_free(ctx);
193+#elif defined(__APPLE__)
194+ CC_SHA256_CTX ctx;
195+ CC_SHA256_Init(&ctx);
196+ ssize_t count = 0;
197+ while ((count = read(readFd, buffer, sizeof(buffer))) > 0) {
198+ CC_SHA256_Update(&ctx, buffer, static_cast<CC_LONG>(count));
199+ }
200+ if (count < 0) {
201+ close(readFd);
202+ throw std::runtime_error("Failed to read file for checksum calculation");
203+ }
204+ unsigned char hash[CC_SHA256_DIGEST_LENGTH];
205+ constexpr unsigned int hashLength = CC_SHA256_DIGEST_LENGTH;
206+ CC_SHA256_Final(hash, &ctx);
207+ constexpr int finalized = 1;
208+#else
209+ close(readFd);
210+ throw std::runtime_error("Unsupported platform for SHA256");
211+#endif
212+ close(readFd);
213+ if (finalized != 1) {
214+ throw std::runtime_error("Failed to finalize SHA256");
215+ }
216+ 
217+ std::stringstream result;
218+ for (unsigned int i = 0; i < hashLength; ++i) {
219+ result << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
220+ }
221+ return result.str();
222+}
223+#endif
224+ 
121} // namespace225} // namespace
122 226 
123void ModelDownloader::SetCACertificatePath(const std::string& ca_path) {227void ModelDownloader::SetCACertificatePath(const std::string& ca_path) {
@@ -135,12 +239,15 @@ ModelDownloader::ModelDownloader(
135 const std::string& url,239 const std::string& url,
136 const std::string& destination_path,240 const std::string& destination_path,
137 int64_t expected_size,241 int64_t expected_size,
138- const std::string& expected_checksum242+ const std::string& expected_checksum,
243+ int destination_fd
139)244)
140 : url_(url)245 : url_(url)
141 , destination_path_(destination_path)246 , destination_path_(destination_path)
142 , expected_checksum_(expected_checksum)247 , expected_checksum_(expected_checksum)
143 , expected_size_(expected_size)248 , expected_size_(expected_size)
249+ , secure_destination_fd_(destination_fd)
250+ , secure_verification_fd_(-1)
144 , curl_(nullptr)251 , curl_(nullptr)
145 , headers_(nullptr)252 , headers_(nullptr)
146 , state_(State::kIdle)253 , state_(State::kIdle)
@@ -151,6 +258,18 @@ ModelDownloader::ModelDownloader(
151}258}
152 259 
153ModelDownloader::~ModelDownloader() {260ModelDownloader::~ModelDownloader() {
261+ if (file_ != nullptr) {
262+ fclose(file_);
263+ file_ = nullptr;
264+ }
265+#if !defined(_WIN32)
266+ if (secure_destination_fd_ >= 0) {
267+ close(secure_destination_fd_);
268+ }
269+ if (secure_verification_fd_ >= 0) {
270+ close(secure_verification_fd_);
271+ }
272+#endif
154 CleanupCurl();273 CleanupCurl();
155}274}
156 275 
@@ -161,8 +280,8 @@ void ModelDownloader::InitCurl() {
161 }280 }
162 281 
163 curl_easy_setopt(curl_, CURLOPT_SSLVERSION, CURL_SSLVERSION_DEFAULT);282 curl_easy_setopt(curl_, CURLOPT_SSLVERSION, CURL_SSLVERSION_DEFAULT);
164- curl_easy_setopt(curl_, CURLOPT_URL, url_.c_str());
165 curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L);283 curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L);
284+ 
166 curl_easy_setopt(curl_, CURLOPT_COOKIEFILE, "");285 curl_easy_setopt(curl_, CURLOPT_COOKIEFILE, "");
167 curl_easy_setopt(curl_, CURLOPT_PROXY, "");286 curl_easy_setopt(curl_, CURLOPT_PROXY, "");
168 287 
@@ -193,6 +312,21 @@ void ModelDownloader::InitCurl() {
193 curl_easy_setopt(curl_, CURLOPT_XFERINFODATA, this);312 curl_easy_setopt(curl_, CURLOPT_XFERINFODATA, this);
194}313}
195 314 
315+void ModelDownloader::ConfigureDownloadUrl(const std::string& curlUrl)
316+{
317+ curl_easy_setopt(curl_, CURLOPT_URL, curlUrl.c_str());
318+ const bool testFileUrl = GetUrlScheme(curlUrl) == "file";
319+#if LIBCURL_VERSION_NUM >= 0x075500
320+ const char* allowedProtocol = testFileUrl ? "file" : "https";
321+ curl_easy_setopt(curl_, CURLOPT_PROTOCOLS_STR, allowedProtocol);
322+ curl_easy_setopt(curl_, CURLOPT_REDIR_PROTOCOLS_STR, allowedProtocol);
323+#else
324+ const long allowedProtocol = testFileUrl ? CURLPROTO_FILE : CURLPROTO_HTTPS;
325+ curl_easy_setopt(curl_, CURLOPT_PROTOCOLS, allowedProtocol);
326+ curl_easy_setopt(curl_, CURLOPT_REDIR_PROTOCOLS, allowedProtocol);
327+#endif
328+}
329+ 
196void ModelDownloader::CleanupCurl() {330void ModelDownloader::CleanupCurl() {
197 if (headers_) {331 if (headers_) {
198 curl_slist_free_all(headers_);332 curl_slist_free_all(headers_);
@@ -206,6 +340,22 @@ void ModelDownloader::CleanupCurl() {
206}340}
207 341 
208DownloadError ModelDownloader::SetupResume() {342DownloadError ModelDownloader::SetupResume() {
343+#if !defined(_WIN32)
344+ if (secure_destination_fd_ >= 0) {
345+ const int64_t existingSize = GetOpenFileSize(secure_destination_fd_);
346+ if (existingSize < 0) {
347+ return DownloadError::FILE_ERROR;
348+ }
349+ if (existingSize == 0 || (expected_size_ > 0 && existingSize >= expected_size_)) {
350+ return ResetOpenFile(secure_destination_fd_);
351+ }
352+ resume_offset_ = existingSize;
353+ curl_easy_setopt(curl_, CURLOPT_RESUME_FROM_LARGE, static_cast<curl_off_t>(resume_offset_));
354+ SMART_SERVE_LOGI(TAG "Resuming download from byte %lld",
355+ static_cast<long long>(resume_offset_));
356+ return DownloadError::OK;
357+ }
358+#endif
209 if (!std::filesystem::exists(destination_path_)) {359 if (!std::filesystem::exists(destination_path_)) {
210 return DownloadError::OK;360 return DownloadError::OK;
211 }361 }
@@ -244,6 +394,13 @@ bool ModelDownloader::ValidateFileSize() {
244 }394 }
245 395 
246 int64_t actual_size = GetFileSize(destination_path_);396 int64_t actual_size = GetFileSize(destination_path_);
397+#if !defined(_WIN32)
398+ const int secureFd = secure_verification_fd_ >= 0
399+ ? secure_verification_fd_ : secure_destination_fd_;
400+ if (secureFd >= 0) {
401+ actual_size = GetOpenFileSize(secureFd);
402+ }
403+#endif
247 if (actual_size != expected_size_) {404 if (actual_size != expected_size_) {
248 SMART_SERVE_LOGE(TAG "File size mismatch: expected %lld, got %lld",405 SMART_SERVE_LOGE(TAG "File size mismatch: expected %lld, got %lld",
249 static_cast<long long>(expected_size_), static_cast<long long>(actual_size));406 static_cast<long long>(expected_size_), static_cast<long long>(actual_size));
@@ -265,6 +422,28 @@ size_t ModelDownloader::WriteCallback(void* contents, size_t size, size_t nmemb,
265 }422 }
266 423 
267 if (!downloader->file_) {424 if (!downloader->file_) {
425+#if !defined(_WIN32)
426+ if (downloader->secure_destination_fd_ >= 0) {
427+ downloader->secure_verification_fd_ = dup(downloader->secure_destination_fd_);
428+ if (downloader->secure_verification_fd_ < 0) {
429+ SMART_SERVE_LOGE(TAG "Failed to duplicate secure destination file (errno=%d: %s)",
430+ errno, std::strerror(errno));
431+ return 0;
432+ }
433+ const char* mode = downloader->resume_offset_ > 0 ? "ab" : "wb";
434+ downloader->file_ = fdopen(downloader->secure_destination_fd_, mode);
435+ if (downloader->file_ == nullptr) {
436+ close(downloader->secure_verification_fd_);
437+ downloader->secure_verification_fd_ = -1;
438+ close(downloader->secure_destination_fd_);
439+ downloader->secure_destination_fd_ = -1;
440+ SMART_SERVE_LOGE(TAG "Failed to open secure destination file (errno=%d: %s)",
441+ errno, std::strerror(errno));
442+ return 0;
443+ }
444+ downloader->secure_destination_fd_ = -1;
445+ } else {
446+#endif
268 const char* mode = downloader->resume_offset_ > 0 ? "ab" : "wb";447 const char* mode = downloader->resume_offset_ > 0 ? "ab" : "wb";
269 downloader->file_ = fopen(downloader->destination_path_.c_str(), mode);448 downloader->file_ = fopen(downloader->destination_path_.c_str(), mode);
270 if (!downloader->file_) {449 if (!downloader->file_) {
@@ -272,6 +451,9 @@ size_t ModelDownloader::WriteCallback(void* contents, size_t size, size_t nmemb,
272 downloader->destination_path_.c_str(), errno, std::strerror(errno));451 downloader->destination_path_.c_str(), errno, std::strerror(errno));
273 return 0;452 return 0;
274 }453 }
454+#if !defined(_WIN32)
455+ }
456+#endif
275 }457 }
276 458 
277 size_t written = fwrite(contents, 1, total_size, downloader->file_);459 size_t written = fwrite(contents, 1, total_size, downloader->file_);
@@ -345,11 +527,47 @@ DownloadError ModelDownloader::Download(ProgressCallback callback) {
345 bytes_written_ = 0;527 bytes_written_ = 0;
346 resume_offset_ = 0;528 resume_offset_ = 0;
347 529 
348- if (expected_size_ > 0 && HasExpectedFileSize(destination_path_, expected_size_)) {530+ std::string curlUrl;
349- const bool checksumMatches = expected_checksum_.empty() ||531+ if (!ResolveAllowedModelDownloadUrl(url_, curlUrl) || !IsValidSha256Checksum(expected_checksum_)) {
350- VerifyChecksum(destination_path_, expected_checksum_) == DownloadError::OK;532+ SMART_SERVE_LOGE(TAG "Refusing download with untrusted URL or invalid SHA-256 checksum");
351- if (checksumMatches) {533+ state_.store(State::kFailed);
352- SMART_SERVE_LOGI(TAG "File already complete: %s", destination_path_.c_str());534+ return DownloadError::INVALID_ARGUMENT;
535+ }
536+ ConfigureDownloadUrl(curlUrl);
537+ 
538+#if !defined(_WIN32)
539+ if (secure_destination_fd_ >= 0 && expected_size_ > 0 &&
540+ GetOpenFileSize(secure_destination_fd_) == expected_size_) {
541+ secure_verification_fd_ = dup(secure_destination_fd_);
542+ if (secure_verification_fd_ < 0) {
543+ state_.store(State::kFailed);
544+ return DownloadError::FILE_ERROR;
545+ }
546+ const DownloadError verifyResult = VerifySecureDestinationChecksum();
547+ if (verifyResult == DownloadError::OK) {
548+ const DownloadError permissionResult = MakeOpenFileReadOnly(secure_verification_fd_);
549+ if (permissionResult == DownloadError::OK) {
550+ SMART_SERVE_LOGI(TAG "File already complete and verified: %s", destination_path_.c_str());
551+ state_.store(State::kCompleted);
552+ return DownloadError::OK;
553+ }
554+ state_.store(State::kFailed);
555+ return permissionResult;
556+ }
557+ close(secure_verification_fd_);
558+ secure_verification_fd_ = -1;
559+ const DownloadError resetResult = ResetOpenFile(secure_destination_fd_);
560+ if (resetResult != DownloadError::OK) {
561+ state_.store(State::kFailed);
562+ return resetResult;
563+ }
564+ }
565+#endif
566+ 
567+ if (secure_destination_fd_ < 0 && expected_size_ > 0 &&
568+ HasExpectedFileSize(destination_path_, expected_size_)) {
569+ if (VerifyChecksum(destination_path_, expected_checksum_) == DownloadError::OK) {
570+ SMART_SERVE_LOGI(TAG "File already complete and verified: %s", destination_path_.c_str());
353 const DownloadError permissionResult = MakeFileReadOnly(destination_path_);571 const DownloadError permissionResult = MakeFileReadOnly(destination_path_);
354 if (permissionResult != DownloadError::OK) {572 if (permissionResult != DownloadError::OK) {
355 state_.store(State::kFailed);573 state_.store(State::kFailed);
@@ -385,11 +603,31 @@ DownloadError ModelDownloader::Download(ProgressCallback callback) {
385 603 
386 if (res != CURLE_OK) {604 if (res != CURLE_OK) {
387 SMART_SERVE_LOGE(TAG "Download failed (CURL error): %s", curl_easy_strerror(res));605 SMART_SERVE_LOGE(TAG "Download failed (CURL error): %s", curl_easy_strerror(res));
388- SMART_SERVE_LOGE(TAG "URL: %s", url_.c_str());606+ SMART_SERVE_LOGE(TAG "URL: %s", RedactUrlForLog(url_).c_str());
389 state_.store(State::kFailed);607 state_.store(State::kFailed);
390 return DownloadError::DOWNLOAD_FAILED;608 return DownloadError::DOWNLOAD_FAILED;
391 }609 }
392 610 
611+ char* effectiveUrl = nullptr;
612+ if (curl_easy_getinfo(curl_, CURLINFO_EFFECTIVE_URL, &effectiveUrl) != CURLE_OK ||
613+ effectiveUrl == nullptr || !IsAllowedModelDownloadRedirect(url_, effectiveUrl)) {
614+ SMART_SERVE_LOGE(TAG "Refusing download redirected to an untrusted URL");
615+ DownloadError removeResult;
616+#if !defined(_WIN32)
617+ const int secureFd = secure_verification_fd_ >= 0
618+ ? secure_verification_fd_ : secure_destination_fd_;
619+ if (secureFd >= 0) {
620+ removeResult = ResetOpenFile(secureFd);
621+ } else {
622+ removeResult = RemoveExistingFile(destination_path_);
623+ }
624+#else
625+ removeResult = RemoveExistingFile(destination_path_);
626+#endif
627+ state_.store(State::kFailed);
628+ return removeResult == DownloadError::OK ? DownloadError::DOWNLOAD_FAILED : removeResult;
629+ }
630+ 
393 long http_code = 0;631 long http_code = 0;
394 curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code);632 curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code);
395 633 
@@ -400,30 +638,73 @@ DownloadError ModelDownloader::Download(ProgressCallback callback) {
400 destination_path_.c_str());638 destination_path_.c_str());
401 } else if (http_code >= 400) {639 } else if (http_code >= 400) {
402 SMART_SERVE_LOGE(TAG "Download failed (HTTP %ld) for URL: %s",640 SMART_SERVE_LOGE(TAG "Download failed (HTTP %ld) for URL: %s",
403- http_code, url_.c_str());641+ http_code, RedactUrlForLog(url_).c_str());
404 state_.store(State::kFailed);642 state_.store(State::kFailed);
405 return DownloadError::DOWNLOAD_FAILED;643 return DownloadError::DOWNLOAD_FAILED;
406 }644 }
407 645 
408 if (!ValidateFileSize()) {646 if (!ValidateFileSize()) {
409- std::remove(destination_path_.c_str());647+ DownloadError removeResult;
410- state_.store(State::kFailed);648+#if !defined(_WIN32)
411- return DownloadError::SIZE_MISMATCH;649+ const int secureFd = secure_verification_fd_ >= 0
412- }650+ ? secure_verification_fd_ : secure_destination_fd_;
413- 651+ if (secureFd >= 0) {
414- if (!expected_checksum_.empty()) {652+ removeResult = ResetOpenFile(secureFd);
415- DownloadError verify_result = VerifyChecksum(destination_path_, expected_checksum_);653+ } else {
416- if (verify_result != DownloadError::OK) {654+ removeResult = RemoveExistingFile(destination_path_);
417- std::remove(destination_path_.c_str());
418- state_.store(State::kFailed);
419- if (verify_result == DownloadError::CHECKSUM_MISMATCH) {
420- return DownloadError::CHECKSUM_MISMATCH;
421- }
422- return DownloadError::DOWNLOAD_FAILED;
423 }655 }
656+#else
657+ removeResult = RemoveExistingFile(destination_path_);
658+#endif
659+ state_.store(State::kFailed);
660+ return removeResult == DownloadError::OK ? DownloadError::SIZE_MISMATCH : removeResult;
424 }661 }
425 662 
426- const DownloadError permissionResult = MakeFileReadOnly(destination_path_);663+ DownloadError verify_result;
664+#if !defined(_WIN32)
665+ if (secure_verification_fd_ >= 0 || secure_destination_fd_ >= 0) {
666+ verify_result = VerifySecureDestinationChecksum();
667+ } else {
668+ verify_result = VerifyChecksum(destination_path_, expected_checksum_);
669+ }
670+#else
671+ verify_result = VerifyChecksum(destination_path_, expected_checksum_);
672+#endif
673+ if (verify_result != DownloadError::OK) {
674+ DownloadError removeResult;
675+#if !defined(_WIN32)
676+ const int secureFd = secure_verification_fd_ >= 0
677+ ? secure_verification_fd_ : secure_destination_fd_;
678+ if (secureFd >= 0) {
679+ removeResult = ResetOpenFile(secureFd);
680+ } else {
681+ removeResult = RemoveExistingFile(destination_path_);
682+ }
683+#else
684+ removeResult = RemoveExistingFile(destination_path_);
685+#endif
686+ state_.store(State::kFailed);
687+ if (removeResult != DownloadError::OK) {
688+ return removeResult;
689+ }
690+ if (verify_result == DownloadError::CHECKSUM_MISMATCH) {
691+ return DownloadError::CHECKSUM_MISMATCH;
692+ }
693+ return DownloadError::DOWNLOAD_FAILED;
694+ }
695+ 
696+ DownloadError permissionResult;
697+#if !defined(_WIN32)
698+ const int secureFd = secure_verification_fd_ >= 0
699+ ? secure_verification_fd_ : secure_destination_fd_;
700+ if (secureFd >= 0) {
701+ permissionResult = MakeOpenFileReadOnly(secureFd);
702+ } else {
703+ permissionResult = MakeFileReadOnly(destination_path_);
704+ }
705+#else
706+ permissionResult = MakeFileReadOnly(destination_path_);
707+#endif
427 if (permissionResult != DownloadError::OK) {708 if (permissionResult != DownloadError::OK) {
428 state_.store(State::kFailed);709 state_.store(State::kFailed);
429 return permissionResult;710 return permissionResult;
@@ -433,6 +714,32 @@ DownloadError ModelDownloader::Download(ProgressCallback callback) {
433 return DownloadError::OK;714 return DownloadError::OK;
434}715}
435 716 
717+DownloadError ModelDownloader::VerifySecureDestinationChecksum() const
718+{
719+#if !defined(_WIN32)
720+ const int secureFd = secure_verification_fd_ >= 0
721+ ? secure_verification_fd_ : secure_destination_fd_;
722+ if (secureFd < 0 || !IsValidSha256Checksum(expected_checksum_)) {
723+ return DownloadError::INVALID_ARGUMENT;
724+ }
725+ try {
726+ const std::string actual = CalculateSHA256FromOpenFile(secureFd);
727+ const std::string expected = NormalizeChecksum(expected_checksum_);
728+ if (actual != expected) {
729+ SMART_SERVE_LOGE(TAG "Checksum mismatch for secure destination: expected %s, got %s",
730+ expected.c_str(), actual.c_str());
731+ return DownloadError::CHECKSUM_MISMATCH;
732+ }
733+ return DownloadError::OK;
734+ } catch (const std::exception& error) {
735+ SMART_SERVE_LOGE(TAG "Checksum calculation failed for secure destination: %s", error.what());
736+ return DownloadError::DOWNLOAD_FAILED;
737+ }
738+#else
739+ return VerifyChecksum(destination_path_, expected_checksum_);
740+#endif
741+}
742+ 
436void ModelDownloader::Cancel() {743void ModelDownloader::Cancel() {
437 State expected = State::kDownloading;744 State expected = State::kDownloading;
438 state_.compare_exchange_strong(expected, State::kCancelled);745 state_.compare_exchange_strong(expected, State::kCancelled);
@@ -583,8 +890,9 @@ std::string ModelDownloader::CalculateSHA256(const std::string& file_path) {
583 890 
584DownloadError ModelDownloader::VerifyChecksum(const std::string& file_path, const std::string& expected_checksum)891DownloadError ModelDownloader::VerifyChecksum(const std::string& file_path, const std::string& expected_checksum)
585{892{
586- if (expected_checksum.empty()) {893+ if (!IsValidSha256Checksum(expected_checksum)) {
587- return DownloadError::OK;894+ SMART_SERVE_LOGE(TAG "Invalid SHA-256 checksum");
895+ return DownloadError::INVALID_ARGUMENT;
588 }896 }
589 897 
590 struct stat st;898 struct stat st;
@@ -69,8 +69,9 @@ public:
69 ModelDownloader(69 ModelDownloader(
70 const std::string& url,70 const std::string& url,
71 const std::string& destination_path,71 const std::string& destination_path,
72- int64_t expected_size = 0,72+ int64_t expected_size,
73- const std::string& expected_checksum = ""73+ const std::string& expected_checksum,
74+ int destination_fd = -1
74 );75 );
75 76 
76 ~ModelDownloader();77 ~ModelDownloader();
@@ -95,14 +96,18 @@ private:
95 );96 );
96 97 
97 void InitCurl();98 void InitCurl();
99+ void ConfigureDownloadUrl(const std::string& curlUrl);
98 void CleanupCurl();100 void CleanupCurl();
99 DownloadError SetupResume();101 DownloadError SetupResume();
100 bool ValidateFileSize();102 bool ValidateFileSize();
103+ DownloadError VerifySecureDestinationChecksum() const;
101 104 
102 std::string url_;105 std::string url_;
103 std::string destination_path_;106 std::string destination_path_;
104 std::string expected_checksum_;107 std::string expected_checksum_;
105 int64_t expected_size_;108 int64_t expected_size_;
109+ int secure_destination_fd_;
110+ int secure_verification_fd_;
106 111 
107 CURL* curl_;112 CURL* curl_;
108 curl_slist* headers_;113 curl_slist* headers_;
@@ -26,9 +26,11 @@
26#include <stdexcept>26#include <stdexcept>
27#include <system_error>27#include <system_error>
28 28 
29+#if !defined(_WIN32)
29#include <fcntl.h>30#include <fcntl.h>
30#include <sys/stat.h>31#include <sys/stat.h>
31#include <unistd.h>32#include <unistd.h>
33+#endif
32 34 
33#include "model/model_util.h"35#include "model/model_util.h"
34#include "nlohmann/json.hpp"36#include "nlohmann/json.hpp"
@@ -267,6 +269,115 @@ bool IsSafeInstalledModelInfo(
267 return true;269 return true;
268}270}
269 271 
272+#if !defined(_WIN32)
273+bool RelativeManagedPath(
274+ const std::string& storageRoot, const std::string& destinationPath, std::filesystem::path& relative)
275+{
276+ std::error_code error;
277+ const auto root = NormalizedAbsolutePath(storageRoot);
278+ const auto destination = NormalizedAbsolutePath(destinationPath);
279+ relative = std::filesystem::relative(destination, root, error);
280+ return !error && IsSafeRelativeSubPath(relative);
281+}
282+ 
283+int OpenOrCreateNoFollowDirectoryTree(const std::filesystem::path& path)
284+{
285+ std::error_code error;
286+ const auto absolutePath = std::filesystem::weakly_canonical(path, error);
287+ if (error || absolutePath.empty()) {
288+ return -1;
289+ }
290+ int currentFd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
291+ if (currentFd < 0) {
292+ return -1;
293+ }
294+ for (const auto& component : absolutePath) {
295+ const std::string name = component.string();
296+ if (name == "/") {
297+ continue;
298+ }
299+ int childFd = openat(currentFd, name.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
300+ if (childFd < 0 && errno == ENOENT) {
301+ if (mkdirat(currentFd, name.c_str(), S_IRWXU) != 0 && errno != EEXIST) {
302+ close(currentFd);
303+ return -1;
304+ }
305+ childFd = openat(currentFd, name.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
306+ }
307+ if (childFd < 0) {
308+ close(currentFd);
309+ return -1;
310+ }
311+ close(currentFd);
312+ currentFd = childFd;
313+ }
314+ return currentFd;
315+}
316+ 
317+int OpenSecureDownloadDestination(const std::string& storageRoot, const std::string& destinationPath)
318+{
319+ std::filesystem::path relative;
320+ if (!RelativeManagedPath(storageRoot, destinationPath, relative)) {
321+ return -1;
322+ }
323+ 
324+ int currentFd = OpenOrCreateNoFollowDirectoryTree(storageRoot);
325+ if (currentFd < 0) {
326+ return -1;
327+ }
328+ 
329+ const auto closeCurrent = [&currentFd]() {
330+ if (currentFd >= 0) {
331+ close(currentFd);
332+ currentFd = -1;
333+ }
334+ };
335+ std::vector<std::string> components;
336+ for (const auto& component : relative) {
337+ components.push_back(component.string());
338+ }
339+ if (components.empty()) {
340+ closeCurrent();
341+ return -1;
342+ }
343+ for (size_t index = 0; index + 1 < components.size(); ++index) {
344+ const std::string& name = components[index];
345+ int childFd = openat(currentFd, name.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
346+ if (childFd < 0 && errno == ENOENT) {
347+ if (mkdirat(currentFd, name.c_str(), S_IRWXU) != 0 && errno != EEXIST) {
348+ closeCurrent();
349+ return -1;
350+ }
351+ childFd = openat(currentFd, name.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
352+ }
353+ if (childFd < 0) {
354+ closeCurrent();
355+ return -1;
356+ }
357+ close(currentFd);
358+ currentFd = childFd;
359+ }
360+ 
361+ const std::string& filename = components.back();
362+ int fileFd = openat(currentFd, filename.c_str(), O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW,
363+ S_IRUSR | S_IWUSR);
364+ if (fileFd < 0 && errno == EACCES) {
365+ const int readFd = openat(currentFd, filename.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
366+ if (readFd >= 0) {
367+ struct stat st;
368+ const bool regular = fstat(readFd, &st) == 0 && S_ISREG(st.st_mode);
369+ const bool writable = regular && fchmod(readFd, st.st_mode | S_IWUSR) == 0;
370+ close(readFd);
371+ if (writable) {
372+ fileFd = openat(currentFd, filename.c_str(), O_RDWR | O_CLOEXEC | O_NOFOLLOW);
373+ }
374+ }
375+ }
376+ closeCurrent();
377+ return fileFd;
378+}
379+#endif
380+ 
270} // namespace381} // namespace
271 382 
272std::string ModelFileManager::ModelFileMetadata::ToJson() const383std::string ModelFileManager::ModelFileMetadata::ToJson() const
@@ -344,7 +455,7 @@ size_t ModelFileManager::GetMaxConcurrentDownloads() const
344 455 
345DownloadError ModelFileManager::ResumeDownload(const std::string& modelId)456DownloadError ModelFileManager::ResumeDownload(const std::string& modelId)
346{457{
347- if (modelId.empty()) {458+ if (!IsValidModelId(modelId)) {
348 return DownloadError::INVALID_ARGUMENT;459 return DownloadError::INVALID_ARGUMENT;
349 }460 }
350 std::lock_guard<std::mutex> lock(mutex_);461 std::lock_guard<std::mutex> lock(mutex_);
@@ -370,7 +481,7 @@ DownloadError ModelFileManager::ResumeDownload(const std::string& modelId)
370 481 
371DownloadError ModelFileManager::CancelDownload(const std::string& modelId)482DownloadError ModelFileManager::CancelDownload(const std::string& modelId)
372{483{
373- if (modelId.empty()) {484+ if (!IsValidModelId(modelId)) {
374 return DownloadError::INVALID_ARGUMENT;485 return DownloadError::INVALID_ARGUMENT;
375 }486 }
376 487 
@@ -428,7 +539,7 @@ DownloadError ModelFileManager::CancelDownload(const std::string& modelId)
428 539 
429DownloadError ModelFileManager::WaitForIdle(const std::string& modelId, uint32_t timeoutMs)540DownloadError ModelFileManager::WaitForIdle(const std::string& modelId, uint32_t timeoutMs)
430{541{
431- if (modelId.empty()) {542+ if (!IsValidModelId(modelId)) {
432 return DownloadError::INVALID_ARGUMENT;543 return DownloadError::INVALID_ARGUMENT;
433 }544 }
434 std::unique_lock<std::mutex> lock(mutex_);545 std::unique_lock<std::mutex> lock(mutex_);
@@ -443,7 +554,7 @@ DownloadError ModelFileManager::WaitForIdle(const std::string& modelId, uint32_t
443 554 
444bool ModelFileManager::HasActiveDownload(const std::string& modelId) const555bool ModelFileManager::HasActiveDownload(const std::string& modelId) const
445{556{
446- if (modelId.empty()) {557+ if (!IsValidModelId(modelId)) {
447 return false;558 return false;
448 }559 }
449 std::lock_guard<std::mutex> lock(mutex_);560 std::lock_guard<std::mutex> lock(mutex_);
@@ -458,7 +569,7 @@ DownloadError ModelFileManager::SaveInstalledModelInfo(
458 const InstalledModelInfo& info,569 const InstalledModelInfo& info,
459 const std::string& metadataPath)570 const std::string& metadataPath)
460{571{
461- if (modelId.empty() || metadataPath.empty() || !HasValidMetadataStringLengths(modelId, info)) {572+ if (!IsValidModelId(modelId) || metadataPath.empty() || !HasValidMetadataStringLengths(modelId, info)) {
462 return DownloadError::INVALID_ARGUMENT;573 return DownloadError::INVALID_ARGUMENT;
463 }574 }
464 if (!IsSafeMetadataPath(metadataPath)) {575 if (!IsSafeMetadataPath(metadataPath)) {
@@ -485,7 +596,7 @@ DownloadError ModelFileManager::RemoveInstalledModelInfo(
485 const std::string& modelId,596 const std::string& modelId,
486 const std::string& metadataPath)597 const std::string& metadataPath)
487{598{
488- if (modelId.empty() || metadataPath.empty()) {599+ if (!IsValidModelId(modelId) || metadataPath.empty()) {
489 return DownloadError::INVALID_ARGUMENT;600 return DownloadError::INVALID_ARGUMENT;
490 }601 }
491 if (!IsSafeMetadataPath(metadataPath)) {602 if (!IsSafeMetadataPath(metadataPath)) {
@@ -505,7 +616,7 @@ std::optional<ModelFileManager::InstalledModelInfo> ModelFileManager::GetInstall
505 const std::string& modelId,616 const std::string& modelId,
506 const std::string& metadataPath) const617 const std::string& metadataPath) const
507{618{
508- if (modelId.empty() || metadataPath.empty()) {619+ if (!IsValidModelId(modelId) || metadataPath.empty()) {
509 return std::nullopt;620 return std::nullopt;
510 }621 }
511 if (!IsSafeMetadataPath(metadataPath)) {622 if (!IsSafeMetadataPath(metadataPath)) {
@@ -573,7 +684,7 @@ DownloadError ModelFileManager::DownloadModelFiles(
573 bool returnOnPause,684 bool returnOnPause,
574 const std::string& storageRoot)685 const std::string& storageRoot)
575{686{
576- if (modelId.empty() || files.empty()) {687+ if (!IsValidModelId(modelId) || files.empty()) {
577 return DownloadError::INVALID_ARGUMENT;688 return DownloadError::INVALID_ARGUMENT;
578 }689 }
579 690 
@@ -589,7 +700,14 @@ DownloadError ModelFileManager::DownloadModelFiles(
589 fileTasks.reserve(files.size());700 fileTasks.reserve(files.size());
590 for (size_t i = 0; i < files.size(); ++i) {701 for (size_t i = 0; i < files.size(); ++i) {
591 const auto& spec = files[i];702 const auto& spec = files[i];
592- if (spec.url.empty() || spec.destinationPath.empty()) {703+ if (spec.url.empty() || spec.destinationPath.empty() || !IsValidSha256Checksum(spec.checksum)) {
704+ SMART_SERVE_LOGE(TAG "invalid download spec model=%s: SHA-256 checksum is mandatory",
705+ modelId.c_str());
706+ return DownloadError::INVALID_ARGUMENT;
707+ }
708+ if (!IsAllowedModelDownloadSource(spec.url)) {
709+ SMART_SERVE_LOGE(TAG "insecure download source model=%s url=%s",
710+ modelId.c_str(), RedactUrlForLog(spec.url).c_str());
593 return DownloadError::INVALID_ARGUMENT;711 return DownloadError::INVALID_ARGUMENT;
594 }712 }
595 if (!IsSafeManagedPath(storageRoot, spec.destinationPath)) {713 if (!IsSafeManagedPath(storageRoot, spec.destinationPath)) {
@@ -609,6 +727,8 @@ DownloadError ModelFileManager::DownloadModelFiles(
609 fileTask->index = i;727 fileTask->index = i;
610 fileTask->key = modelId + "#" + std::to_string(i);728 fileTask->key = modelId + "#" + std::to_string(i);
611 fileTask->spec = spec;729 fileTask->spec = spec;
730+ fileTask->storageRoot = storageRoot;
731+ fileTask->spec.checksum = NormalizeChecksum(spec.checksum);
612 fileTask->owner = modelTask;732 fileTask->owner = modelTask;
613 fileTasks.push_back(std::move(fileTask));733 fileTasks.push_back(std::move(fileTask));
614 }734 }
@@ -650,7 +770,7 @@ DownloadError ModelFileManager::DownloadModelFiles(
650 770 
651DownloadError ModelFileManager::PauseDownload(const std::string& modelId)771DownloadError ModelFileManager::PauseDownload(const std::string& modelId)
652{772{
653- if (modelId.empty()) {773+ if (!IsValidModelId(modelId)) {
654 return DownloadError::INVALID_ARGUMENT;774 return DownloadError::INVALID_ARGUMENT;
655 }775 }
656 776 
@@ -797,16 +917,37 @@ void ModelFileManager::WorkerLoop()
797 917 
798void ModelFileManager::RunFileTask(const std::shared_ptr<FileDownloadTask>& task)918void ModelFileManager::RunFileTask(const std::shared_ptr<FileDownloadTask>& task)
799{919{
920+#if !defined(_WIN32)
921+ const int destinationFd = OpenSecureDownloadDestination(task->storageRoot, task->spec.destinationPath);
922+ if (destinationFd < 0) {
923+ std::lock_guard<std::mutex> lock(mutex_);
924+ CompleteFileTaskLocked(task, DownloadError::FILE_ERROR);
925+ return;
926+ }
927+#else
800 const auto parent = std::filesystem::path(task->spec.destinationPath).parent_path();928 const auto parent = std::filesystem::path(task->spec.destinationPath).parent_path();
801 if (!parent.empty()) {929 if (!parent.empty()) {
802 std::filesystem::create_directories(parent);930 std::filesystem::create_directories(parent);
803 }931 }
932+ constexpr int destinationFd = -1;
933+#endif
804 934 
805- auto downloader = std::make_shared<ModelDownloader>(935+ std::shared_ptr<ModelDownloader> downloader;
806- task->spec.url,936+ try {
807- task->spec.destinationPath,937+ downloader = std::make_shared<ModelDownloader>(
808- task->spec.expectedSize,938+ task->spec.url,
809- task->spec.checksum);939+ task->spec.destinationPath,
940+ task->spec.expectedSize,
941+ task->spec.checksum,
942+ destinationFd);
943+ } catch (...) {
944+#if !defined(_WIN32)
945+ close(destinationFd);
946+#endif
947+ std::lock_guard<std::mutex> lock(mutex_);
948+ CompleteFileTaskLocked(task, DownloadError::UNKNOWN);
949+ return;
950+ }
810 {951 {
811 std::lock_guard<std::mutex> lock(mutex_);952 std::lock_guard<std::mutex> lock(mutex_);
812 task->downloader = downloader;953 task->downloader = downloader;
@@ -932,7 +1073,7 @@ bool ModelFileManager::LoadInstalledModelsMetadata(
932 }1073 }
933 for (auto it = modelItems.begin(); it != modelItems.end(); ++it) {1074 for (auto it = modelItems.begin(); it != modelItems.end(); ++it) {
934 const auto& modelValue = it.value();1075 const auto& modelValue = it.value();
935- if (!modelValue.is_object()) {1076+ if (!IsValidModelId(it.key()) || !modelValue.is_object()) {
936 continue;1077 continue;
937 }1078 }
938 const auto sizeIt = modelValue.find("size_bytes");1079 const auto sizeIt = modelValue.find("size_bytes");
@@ -88,9 +88,9 @@ public:
88 DownloadError DownloadModelFiles(88 DownloadError DownloadModelFiles(
89 const std::string& modelId,89 const std::string& modelId,
90 const std::vector<FileDownloadSpec>& files,90 const std::vector<FileDownloadSpec>& files,
91- ProgressCallback progressCb = nullptr,91+ ProgressCallback progressCb,
92- bool returnOnPause = false,92+ bool returnOnPause,
93- const std::string& storageRoot = "");93+ const std::string& storageRoot);
94 94 
95 DownloadError PauseDownload(const std::string& modelId);95 DownloadError PauseDownload(const std::string& modelId);
96 DownloadError ResumeDownload(const std::string& modelId);96 DownloadError ResumeDownload(const std::string& modelId);
@@ -128,6 +128,7 @@ private:
128 std::string modelId;128 std::string modelId;
129 size_t index = 0;129 size_t index = 0;
130 FileDownloadSpec spec;130 FileDownloadSpec spec;
131+ std::string storageRoot;
131 TaskState state = TaskState::PENDING;132 TaskState state = TaskState::PENDING;
132 DownloadError result = DownloadError::OK;133 DownloadError result = DownloadError::OK;
133 bool pauseRequested = false;134 bool pauseRequested = false;
@@ -15,16 +15,216 @@
15 15 
16#include "model/model_util.h"16#include "model/model_util.h"
17 17 
18+#include <algorithm>
19+#include <cctype>
20+#include <system_error>
21+#include <vector>
22+ 
23+#include <curl/curl.h>
hb
hbhb8月18日

新增了 <curl/curl.h>,并从第 76 行开始调用 curl_url()、curl_url_get()、curl_free() 等符号。

该文件同时属于 OpenHarmony 的 lms GN 目标,而 services/src/server/BUILD.gn 没有增加 curl 的 external_deps

OpenHarmony 构建会面临头文件不可见或链接阶段未定义符号。

likedislike
8月18日 评论:
24+ 
25+#if !defined(_WIN32)
26+#include <dirent.h>
27+#include <fcntl.h>
28+#include <sys/stat.h>
29+#include <unistd.h>
30+#endif
31+ 
18namespace OHOS {32namespace OHOS {
19namespace SmartServe {33namespace SmartServe {
20 34 
21std::string NormalizeChecksum(const std::string& checksum)35std::string NormalizeChecksum(const std::string& checksum)
22{36{
23 const std::string prefix = "sha256:";37 const std::string prefix = "sha256:";
24- if (checksum.rfind(prefix, 0) == 0) {38+ std::string normalized = checksum.rfind(prefix, 0) == 0
25- return checksum.substr(prefix.size());39+ ? checksum.substr(prefix.size()) : checksum;
40+ std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char value) {
41+ return static_cast<char>(std::tolower(value));
42+ });
43+ return normalized;
44+}
45+ 
46+bool IsValidSha256Checksum(const std::string& checksum)
47+{
48+ const std::string normalized = NormalizeChecksum(checksum);
49+ return normalized.size() == 64 &&
50+ std::all_of(normalized.begin(), normalized.end(), [](unsigned char value) {
51+ return std::isxdigit(value) != 0;
52+ });
53+}
54+ 
55+std::string GetUrlScheme(const std::string& url)
56+{
57+ const auto separator = url.find(':');
58+ if (separator == std::string::npos || separator == 0) {
59+ return "";
26 }60 }
27- return checksum;61+ std::string scheme = url.substr(0, separator);
62+ std::transform(scheme.begin(), scheme.end(), scheme.begin(), [](unsigned char value) {
63+ return static_cast<char>(std::tolower(value));
64+ });
65+ return scheme;
66+}
67+ 
68+bool IsSecureModelDownloadUrl(const std::string& url)
69+{
70+ const size_t schemeEnd = url.find(':');
71+ if (GetUrlScheme(url) != "https" || schemeEnd == std::string::npos ||
72+ url.compare(schemeEnd, 3, "://") != 0) {
73+ return false;
74+ }
75+ const size_t authorityStart = schemeEnd + 3;
76+ if (authorityStart >= url.size() || url.find_first_of("/?#", authorityStart) == authorityStart) {
77+ return false;
78+ }
79+ if (std::any_of(url.begin(), url.end(), [](unsigned char value) {
80+ return std::iscntrl(value) != 0 || std::isspace(value) != 0;
81+ })) {
82+ return false;
83+ }
84+ 
85+ CURLU* parsed = curl_url();
86+ if (parsed == nullptr) {
87+ return false;
88+ }
89+ const auto cleanup = [&parsed]() {
90+ curl_url_cleanup(parsed);
91+ parsed = nullptr;
92+ };
93+ if (curl_url_set(parsed, CURLUPART_URL, url.c_str(), 0) != CURLUE_OK) {
94+ cleanup();
95+ return false;
96+ }
97+ 
98+ char* scheme = nullptr;
99+ char* host = nullptr;
100+ char* user = nullptr;
101+ char* password = nullptr;
102+ const bool hasScheme = curl_url_get(parsed, CURLUPART_SCHEME, &scheme, 0) == CURLUE_OK;
103+ const bool hasHost = curl_url_get(parsed, CURLUPART_HOST, &host, 0) == CURLUE_OK;
104+ const bool hasUser = curl_url_get(parsed, CURLUPART_USER, &user, 0) == CURLUE_OK;
105+ const bool hasPassword = curl_url_get(parsed, CURLUPART_PASSWORD, &password, 0) == CURLUE_OK;
106+ const bool allowed = hasScheme && hasHost && scheme != nullptr && host != nullptr &&
107+ std::string(scheme) == "https" && host[0] != '\0' &&
108+ (!hasUser || user == nullptr || user[0] == '\0') &&
109+ (!hasPassword || password == nullptr || password[0] == '\0');
110+ 
111+ curl_free(scheme);
112+ curl_free(host);
113+ curl_free(user);
114+ curl_free(password);
115+ cleanup();
116+ return allowed;
117+}
118+ 
119+bool IsSecureModelDownloadRedirect(const std::string& originalUrl, const std::string& effectiveUrl)
120+{
121+ return IsSecureModelDownloadUrl(originalUrl) && IsSecureModelDownloadUrl(effectiveUrl);
122+}
123+ 
124+bool ResolveAllowedModelDownloadUrl(const std::string& configuredUrl, std::string& curlUrl)
125+{
126+ if (IsSecureModelDownloadUrl(configuredUrl)) {
127+ curlUrl = configuredUrl;
128+ return true;
129+ }
130+#ifdef SMARTSERVE_TEST_FILE_URLS_ENABLED
131+ constexpr char TEST_FILE_PREFIX[] = "file+test:///";
132+ constexpr size_t TEST_FILE_SCHEME_SIZE = sizeof("file+test://") - 1;
133+ constexpr size_t TEST_FILE_PREFIX_SIZE = sizeof(TEST_FILE_PREFIX) - 1;
134+ if (configuredUrl.size() > TEST_FILE_PREFIX_SIZE &&
135+ configuredUrl.compare(0, TEST_FILE_PREFIX_SIZE, TEST_FILE_PREFIX) == 0 &&
136+ configuredUrl.find_first_of("?#", TEST_FILE_PREFIX_SIZE) == std::string::npos &&
137+ std::none_of(configuredUrl.begin(), configuredUrl.end(), [](unsigned char value) {
138+ return std::iscntrl(value) != 0;
139+ })) {
140+ curlUrl = "file://" + configuredUrl.substr(TEST_FILE_SCHEME_SIZE);
141+ return true;
142+ }
143+#endif
144+ curlUrl.clear();
145+ return false;
146+}
147+ 
148+bool IsAllowedModelDownloadRedirect(const std::string& configuredUrl, const std::string& effectiveUrl)
149+{
150+ std::string curlUrl;
151+ if (!ResolveAllowedModelDownloadUrl(configuredUrl, curlUrl)) {
152+ return false;
153+ }
154+ if (IsSecureModelDownloadUrl(curlUrl)) {
155+ return IsSecureModelDownloadRedirect(curlUrl, effectiveUrl);
156+ }
157+#ifdef SMARTSERVE_TEST_FILE_URLS_ENABLED
158+ return GetUrlScheme(curlUrl) == "file" && GetUrlScheme(effectiveUrl) == "file";
159+#else
160+ return false;
161+#endif
162+}
163+ 
164+bool IsAllowedModelDownloadSource(const std::string& url)
165+{
166+ std::string curlUrl;
167+ return ResolveAllowedModelDownloadUrl(url, curlUrl);
168+}
169+ 
170+std::string RedactUrlForLog(const std::string& url)
171+{
172+ CURLU* parsed = curl_url();
173+ if (parsed == nullptr) {
174+ return "<invalid-url>";
175+ }
176+ const auto cleanup = [&parsed]() {
177+ curl_url_cleanup(parsed);
178+ parsed = nullptr;
179+ };
180+ if (curl_url_set(parsed, CURLUPART_URL, url.c_str(), 0) != CURLUE_OK) {
181+ cleanup();
182+ return "<invalid-url>";
183+ }
184+ 
185+ char* scheme = nullptr;
186+ char* host = nullptr;
187+ char* port = nullptr;
188+ char* path = nullptr;
189+ const bool hasScheme = curl_url_get(parsed, CURLUPART_SCHEME, &scheme, 0) == CURLUE_OK;
190+ const bool hasHost = curl_url_get(parsed, CURLUPART_HOST, &host, 0) == CURLUE_OK;
191+ const bool hasPort = curl_url_get(parsed, CURLUPART_PORT, &port, 0) == CURLUE_OK;
192+ const bool hasPath = curl_url_get(parsed, CURLUPART_PATH, &path, 0) == CURLUE_OK;
193+ if (!hasScheme || scheme == nullptr || scheme[0] == '\0') {
194+ curl_free(scheme);
195+ curl_free(host);
196+ curl_free(port);
197+ curl_free(path);
198+ cleanup();
199+ return "<invalid-url>";
200+ }
201+ 
202+ std::string redacted = scheme;
203+ redacted += "://";
204+ if (hasHost && host != nullptr) {
205+ const std::string hostValue(host);
206+ if (hostValue.find(':') != std::string::npos) {
207+ redacted += "[" + hostValue + "]";
208+ } else {
209+ redacted += hostValue;
210+ }
211+ }
212+ if (hasPort && port != nullptr && port[0] != '\0') {
213+ redacted += ":";
214+ redacted += port;
215+ }
216+ if (hasPath && path != nullptr && path[0] != '\0') {
217+ redacted += path;
218+ } else {
219+ redacted += "/";
220+ }
221+ 
222+ curl_free(scheme);
223+ curl_free(host);
224+ curl_free(port);
225+ curl_free(path);
226+ cleanup();
227+ return redacted;
28}228}
29 229 
30std::filesystem::path NormalizedAbsolutePath(const std::filesystem::path& path)230std::filesystem::path NormalizedAbsolutePath(const std::filesystem::path& path)
@@ -188,6 +388,180 @@ bool ResolveModelFilePath(
188 return ResolveSafeChildPath(modelBasePath, filename, outPath);388 return ResolveSafeChildPath(modelBasePath, filename, outPath);
189}389}
190 390 
391+bool ResolveSafeDeletionPath(
392+ const std::string& modelDir,
393+ const std::string& candidatePath,
394+ std::string& outPath)
395+{
396+ if (modelDir.empty() || candidatePath.empty()) {
397+ return false;
398+ }
399+ 
400+ std::error_code error;
401+ const auto canonicalRoot = std::filesystem::weakly_canonical(modelDir, error);
402+ if (error || canonicalRoot.empty()) {
403+ return false;
404+ }
405+ const auto canonicalCandidate = std::filesystem::weakly_canonical(candidatePath, error);
406+ if (error || canonicalCandidate.empty() || canonicalCandidate == canonicalRoot ||
407+ !IsPathWithinRoot(canonicalRoot, canonicalCandidate)) {
408+ return false;
409+ }
410+ 
411+ const auto status = std::filesystem::symlink_status(candidatePath, error);
412+ if (!error && std::filesystem::is_symlink(status)) {
413+ return false;
414+ }
415+ if (error && error != std::errc::no_such_file_or_directory) {
416+ return false;
417+ }
418+ 
419+ outPath = canonicalCandidate.string();
420+ return true;
421+}
422+ 
423+#if !defined(_WIN32)
424+namespace {
425+ 
426+bool RemoveDirectoryContentsNoFollow(int directoryFd)
427+{
428+ const int scanFd = dup(directoryFd);
429+ if (scanFd < 0) {
430+ return false;
431+ }
432+ DIR* directory = fdopendir(scanFd);
433+ if (directory == nullptr) {
434+ close(scanFd);
435+ return false;
436+ }
437+ 
438+ bool success = true;
439+ while (success) {
440+ errno = 0;
441+ dirent* entry = readdir(directory);
442+ if (entry == nullptr) {
443+ success = errno == 0;
444+ break;
445+ }
446+ const std::string name(entry->d_name);
447+ if (name == "." || name == "..") {
448+ continue;
449+ }
450+ 
451+ struct stat status;
452+ if (fstatat(directoryFd, name.c_str(), &status, AT_SYMLINK_NOFOLLOW) != 0 ||
453+ S_ISLNK(status.st_mode)) {
454+ success = false;
455+ break;
456+ }
457+ if (S_ISDIR(status.st_mode)) {
458+ const int childFd = openat(directoryFd, name.c_str(),
459+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
460+ if (childFd < 0) {
461+ success = false;
462+ break;
463+ }
464+ success = RemoveDirectoryContentsNoFollow(childFd);
465+ close(childFd);
466+ if (!success || unlinkat(directoryFd, name.c_str(), AT_REMOVEDIR) != 0) {
467+ success = false;
468+ break;
469+ }
470+ } else if (unlinkat(directoryFd, name.c_str(), 0) != 0) {
471+ success = false;
472+ break;
473+ }
474+ }
475+ closedir(directory);
476+ return success;
477+}
478+ 
479+bool RelativePathWithinRoot(
480+ const std::string& rootPath, const std::string& candidatePath, std::filesystem::path& relative)
481+{
482+ std::error_code error;
483+ const auto root = NormalizedAbsolutePath(rootPath);
484+ const auto candidate = NormalizedAbsolutePath(candidatePath);
485+ relative = std::filesystem::relative(candidate, root, error);
486+ return !error && IsSafeRelativeSubPath(relative);
487+}
488+ 
489+} // namespace
490+#endif
491+ 
492+bool RemoveManagedPathNoFollow(
493+ const std::string& modelDir, const std::string& candidatePath)
494+{
495+#if defined(_WIN32)
496+ (void)modelDir;
497+ (void)candidatePath;
498+ return false;
499+#else
500+ std::filesystem::path relative;
501+ if (!RelativePathWithinRoot(modelDir, candidatePath, relative)) {
502+ return false;
503+ }
504+ int currentFd = open(NormalizedAbsolutePath(modelDir).c_str(),
505+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
506+ if (currentFd < 0) {
507+ return false;
508+ }
509+ const auto closeCurrent = [&currentFd]() {
510+ if (currentFd >= 0) {
511+ close(currentFd);
512+ currentFd = -1;
513+ }
514+ };
515+ 
516+ std::vector<std::string> components;
517+ for (const auto& component : relative) {
518+ components.push_back(component.string());
519+ }
520+ if (components.empty()) {
521+ closeCurrent();
522+ return false;
523+ }
524+ for (size_t index = 0; index + 1 < components.size(); ++index) {
525+ const int childFd = openat(currentFd, components[index].c_str(),
526+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
527+ if (childFd < 0) {
528+ closeCurrent();
529+ return errno == ENOENT;
530+ }
531+ close(currentFd);
532+ currentFd = childFd;
533+ }
534+ 
535+ const std::string& name = components.back();
536+ struct stat status;
537+ if (fstatat(currentFd, name.c_str(), &status, AT_SYMLINK_NOFOLLOW) != 0) {
538+ const bool missing = errno == ENOENT;
539+ closeCurrent();
540+ return missing;
541+ }
542+ if (S_ISLNK(status.st_mode)) {
543+ closeCurrent();
544+ return false;
545+ }
546+ if (!S_ISDIR(status.st_mode)) {
547+ const bool removed = unlinkat(currentFd, name.c_str(), 0) == 0;
548+ closeCurrent();
549+ return removed;
550+ }
551+ 
552+ const int targetFd = openat(currentFd, name.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
553+ if (targetFd < 0) {
554+ closeCurrent();
555+ return false;
556+ }
557+ const bool removed = RemoveDirectoryContentsNoFollow(targetFd) &&
558+ unlinkat(currentFd, name.c_str(), AT_REMOVEDIR) == 0;
559+ close(targetFd);
560+ closeCurrent();
561+ return removed;
562+#endif
563+}
564+ 
191std::string InstalledMetadataPath(const std::string& modelDir)565std::string InstalledMetadataPath(const std::string& modelDir)
192{566{
193 return (NormalizedAbsolutePath(modelDir) / "installed_models.json").string();567 return (NormalizedAbsolutePath(modelDir) / "installed_models.json").string();
@@ -25,6 +25,16 @@ namespace OHOS {
25namespace SmartServe {25namespace SmartServe {
26 26 
27std::string NormalizeChecksum(const std::string& checksum);27std::string NormalizeChecksum(const std::string& checksum);
28+bool IsValidSha256Checksum(const std::string& checksum);
29+std::string GetUrlScheme(const std::string& url);
30+bool IsSecureModelDownloadUrl(const std::string& url);
31+bool IsSecureModelDownloadRedirect(const std::string& originalUrl, const std::string& effectiveUrl);
32+// Validates a configured model URL and returns the URL safe to pass to curl.
33+// Production accepts HTTPS only; test builds additionally accept file+test:/// paths.
34+bool ResolveAllowedModelDownloadUrl(const std::string& configuredUrl, std::string& curlUrl);
35+bool IsAllowedModelDownloadRedirect(const std::string& configuredUrl, const std::string& effectiveUrl);
36+bool IsAllowedModelDownloadSource(const std::string& url);
37+std::string RedactUrlForLog(const std::string& url);
28 38 
29std::filesystem::path NormalizedAbsolutePath(const std::filesystem::path& path);39std::filesystem::path NormalizedAbsolutePath(const std::filesystem::path& path);
30bool HasUnsafePathComponent(const std::filesystem::path& path);40bool HasUnsafePathComponent(const std::filesystem::path& path);
@@ -59,6 +69,17 @@ bool ResolveModelFilePath(
59 const std::string& modelBasePath,69 const std::string& modelBasePath,
60 const std::string& filename,70 const std::string& filename,
61 std::string& outPath);71 std::string& outPath);
72+// Resolves an existing or pending managed path for destructive operations.
73+// Unlike ResolveSafeChildPath(), this rejects paths that traverse symlinks.
74+bool ResolveSafeDeletionPath(
75+ const std::string& modelDir,
76+ const std::string& candidatePath,
77+ std::string& outPath);
78+// Removes a managed file or directory by walking from an opened model-root
79+// descriptor, rejecting symlinks in every path component.
80+bool RemoveManagedPathNoFollow(
81+ const std::string& modelDir,
82+ const std::string& candidatePath);
62 83 
63std::string InstalledMetadataPath(const std::string& modelDir);84std::string InstalledMetadataPath(const std::string& modelDir);
64 85 
@@ -19,6 +19,7 @@
19 19 
20#include "service/service.h"20#include "service/service.h"
21#include "model/model_config.h"21#include "model/model_config.h"
22+#include "model/model_file_manager.h"
22#include "model/model_file_operation_coordinator.h"23#include "model/model_file_operation_coordinator.h"
23#include "model/model_util.h"24#include "model/model_util.h"
24#include "scheduler/basescheduler.h"25#include "scheduler/basescheduler.h"
@@ -96,6 +97,30 @@ bool ResolveModelConfigPathOrLog(std::string& configPath, const char* failureAct
96 return false;97 return false;
97}98}
98 99 
100+#if !defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
101+bool HasVerifiedManagedDownload(
102+ const ModelConfig& descriptor, const std::string& modelDir, const std::filesystem::path& modelPath)
103+{
104+ const auto info = ModelFileManager::Instance().GetInstalledModelInfo(
105+ descriptor.id, InstalledMetadataPath(modelDir));
106+ if (!info || info->status != "downloaded" || !info->verified || info->engine != descriptor.engine) {
107+ return false;
108+ }
109+ 
110+ std::string registeredPath;
111+ if (!ResolveSafeDeletionPath(modelDir, info->path, registeredPath) ||
112+ registeredPath != modelPath.string()) {
113+ return false;
114+ }
115+ 
116+ if (descriptor.files.empty()) {
117+ const std::string checksum = NormalizeChecksum(descriptor.checksum);
118+ return IsValidSha256Checksum(checksum) && NormalizeChecksum(info->checksum) == checksum;
119+ }
120+ return true;
121+}
122+#endif
123+ 
99// Caller must hold Service::msMgmtMutex_.124// Caller must hold Service::msMgmtMutex_.
100bool TryRegisterModelLocked(Service* service, ModelConfig descriptor, const std::string& modelDir)125bool TryRegisterModelLocked(Service* service, ModelConfig descriptor, const std::string& modelDir)
101{126{
@@ -120,6 +145,12 @@ bool TryRegisterModelLocked(Service* service, ModelConfig descriptor, const std:
120 descriptor.id.c_str(), descriptor.engine.c_str());145 descriptor.id.c_str(), descriptor.engine.c_str());
121 return true;146 return true;
122 }147 }
148+#if !defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
149+ if (!hasOnDiskArtifacts) {
150+ SMART_SERVE_LOGE(TAG "Refusing preinstalled model without a download manifest: %s", descriptor.id.c_str());
151+ return false;
152+ }
153+#endif
123 154 
124 auto stableFileOperation =155 auto stableFileOperation =
125 ModelFileOperationCoordinator::Instance().TryAcquireStableOperation(descriptor.id);156 ModelFileOperationCoordinator::Instance().TryAcquireStableOperation(descriptor.id);
@@ -182,6 +213,14 @@ bool TryRegisterModelLocked(Service* service, ModelConfig descriptor, const std:
182 descriptor.id.c_str(), modelPath.c_str());213 descriptor.id.c_str(), modelPath.c_str());
183 return false;214 return false;
184 }215 }
216+ 
217+#if !defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
218+ if (!HasVerifiedManagedDownload(descriptor, modelDir, modelPath)) {
219+ SMART_SERVE_LOGE(TAG "Model was not downloaded and verified by SmartServe: %s", descriptor.id.c_str());
220+ return false;
221+ }
222+ descriptor.managed_download_verified = true;
223+#endif
185 descriptor.name = modelPath.string();224 descriptor.name = modelPath.string();
186 225 
187 service->modelManager_.RegisterModel(descriptor);226 service->modelManager_.RegisterModel(descriptor);
@@ -86,6 +86,12 @@ smartserve_add_gtest(smartserve_internal_test internal
86 internal/service_lifecycle_test.cpp86 internal/service_lifecycle_test.cpp
87 internal/task_stream_test.cpp87 internal/task_stream_test.cpp
88)88)
89+if(SMARTSERVE_ENABLE_TEST_FILE_URLS)
90+ target_compile_definitions(smartserve_internal_test PRIVATE SMARTSERVE_TEST_FILE_URLS_ENABLED)
91+endif()
92+if(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS)
93+ target_compile_definitions(smartserve_internal_test PRIVATE SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
94+endif()
89 95 
90smartserve_add_gtest(smartserve_interface_test interface96smartserve_add_gtest(smartserve_interface_test interface
91 interface/client_api_test.cpp97 interface/client_api_test.cpp
@@ -138,6 +144,12 @@ if(TARGET smartserve_sdk)
138 target_compile_definitions(smartserve_api_test PRIVATE144 target_compile_definitions(smartserve_api_test PRIVATE
139 "SMARTSERVE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\""145 "SMARTSERVE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\""
140 )146 )
147+ if(SMARTSERVE_ENABLE_TEST_FILE_URLS)
148+ target_compile_definitions(smartserve_api_test PRIVATE SMARTSERVE_TEST_FILE_URLS_ENABLED)
149+ endif()
150+ if(SMARTSERVE_ENABLE_TEST_PREINSTALLED_MODELS)
151+ target_compile_definitions(smartserve_api_test PRIVATE SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
152+ endif()
141 smartserve_apply_test_rpath(smartserve_api_test)153 smartserve_apply_test_rpath(smartserve_api_test)
142 gtest_discover_tests(smartserve_api_test154 gtest_discover_tests(smartserve_api_test
143 TEST_PREFIX "api."155 TEST_PREFIX "api."
@@ -139,6 +139,24 @@ TEST_F(GewuSmartServeApiTest, ModelsDirectoryRequiresCanonicalAbsolutePath)
139 std::filesystem::canonical(baseDir / "models"));139 std::filesystem::canonical(baseDir / "models"));
140}140}
141 141 
142+// Verifies production rejects model roots writable by users outside the owning application.
143+TEST_F(GewuSmartServeApiTest, ModelsDirectoryRejectsGroupOrWorldWritableRoot)
144+{
145+#if defined(_WIN32) || defined(SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS)
146+ GTEST_SKIP() << "private-directory ownership is enforced only in production POSIX builds";
147+#else
148+ const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_untrusted_models_dir";
149+ std::filesystem::remove_all(baseDir);
150+ std::filesystem::create_directories(baseDir);
151+ std::filesystem::permissions(baseDir, std::filesystem::perms::group_write,
152+ std::filesystem::perm_options::add);
153+ 
154+ EXPECT_EQ(GewuSmartServeSetModelsDirectory(baseDir.string().c_str()),
155+ GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
156+ std::filesystem::remove_all(baseDir);
157+#endif
158+}
159+ 
142TEST_F(GewuSmartServeApiTest, InitializeFinalizeAreIdempotent)160TEST_F(GewuSmartServeApiTest, InitializeFinalizeAreIdempotent)
143{161{
144 EXPECT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);162 EXPECT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
@@ -675,6 +693,11 @@ TEST_F(GewuSmartServeApiTest, GetModelInfoAndDownloadInvalidModel)
675 EXPECT_EQ(GewuSmartServeDownloadModel("missing-model", nullptr, nullptr),693 EXPECT_EQ(GewuSmartServeDownloadModel("missing-model", nullptr, nullptr),
676 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);694 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
677 EXPECT_EQ(GewuSmartServeDeleteModel("missing-model"), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);695 EXPECT_EQ(GewuSmartServeDeleteModel("missing-model"), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
696+ EXPECT_EQ(GewuSmartServeDownloadModel("../unsafe", nullptr, nullptr),
697+ GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
698+ EXPECT_EQ(GewuSmartServePauseDownload("../unsafe"), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
699+ EXPECT_EQ(GewuSmartServeDeleteModel("../unsafe"), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
700+ EXPECT_EQ(GewuSmartServeUnloadModel("../unsafe"), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
678}701}
679 702 
680// Verifies concurrent model-list and error-string calls remain thread-safe.703// Verifies concurrent model-list and error-string calls remain thread-safe.
@@ -21,6 +21,7 @@
21#include <vector>21#include <vector>
22 22 
23#include "gewu_smartserve/core.h"23#include "gewu_smartserve/core.h"
24+#include "model/model_downloader.h"
24#include "support/mock_applefm_engine.h"25#include "support/mock_applefm_engine.h"
25 26 
26namespace OHOS::SmartServe::Test {27namespace OHOS::SmartServe::Test {
@@ -55,6 +56,10 @@ std::string MakeAppleFmModelsConfig(const std::string &modelId,
55 R"("}]})";56 R"("}]})";
56}57}
57 58 
59+std::string Sha256(const std::filesystem::path &path) {
60+ return OHOS::SmartServe::ModelDownloader::CalculateSHA256(path.string());
61+}
62+ 
58void PauseOnProgress(const char *modelId, int64_t, int64_t, float,63void PauseOnProgress(const char *modelId, int64_t, int64_t, float,
59 void *userData) {64 void *userData) {
60 auto *context = static_cast<PauseDownloadContext *>(userData);65 auto *context = static_cast<PauseDownloadContext *>(userData);
@@ -102,9 +107,10 @@ std::string MakeFileDownloadConfig(const std::string &modelId,
102 std::size_t size) {107 std::size_t size) {
103 return R"({"models":[{"id":")" + modelId +108 return R"({"models":[{"id":")" + modelId +
104 R"(","name":"downloaded.bin","engine":")" + engineName +109 R"(","name":"downloaded.bin","engine":")" + engineName +
105- R"(","base_url":"file://)" + sourcePath.string() +110+ R"(","base_url":"file+test://)" + sourcePath.string() +
106 R"(","model_path":")" + modelId + R"(","size":)" +111 R"(","model_path":")" + modelId + R"(","size":)" +
107- std::to_string(size) + R"(}]})";112+ std::to_string(size) + R"(,"checksum":")" + Sha256(sourcePath) +
113+ R"("}]})";
108}114}
109 115 
110void CompleteMockChat(const std::string &modelId) {116void CompleteMockChat(const std::string &modelId) {
@@ -31,6 +31,7 @@ extern const char *const kModelsConfig;
31std::string ReadExpectedSdkVersion();31std::string ReadExpectedSdkVersion();
32std::string MakeAppleFmModelsConfig(const std::string &modelId,32std::string MakeAppleFmModelsConfig(const std::string &modelId,
33 const std::string &engineName);33 const std::string &engineName);
34+std::string Sha256(const std::filesystem::path &path);
34 35 
35struct PauseDownloadContext {36struct PauseDownloadContext {
36 const char *modelId = nullptr;37 const char *modelId = nullptr;
@@ -50,8 +50,44 @@ bool IsReadOnlyFile(const std::filesystem::path& path)
50 return !error && (permissions & writePermissions) == std::filesystem::perms::none;50 return !error && (permissions & writePermissions) == std::filesystem::perms::none;
51}51}
52 52 
53+// Verifies production rejects a model copied into managed storage without a verified download record.
54+TEST_F(GewuSmartServeApiTest, PreinstalledModelWithoutVerifiedDownloadIsNotLoadable)
55+{
56+#ifdef SMARTSERVE_TEST_TRUST_PREINSTALLED_MODELS
57+ GTEST_SKIP() << "test runtime build intentionally trusts preinstalled model files";
58+#endif
59+ constexpr const char* kEngineName = "unmanaged-model-engine";
60+ const std::string modelId = "unmanaged-model";
61+ const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_unmanaged_model";
62+ const auto modelDir = baseDir / "models";
63+ const auto modelPath = modelDir / modelId / "weights.bin";
64+ std::filesystem::remove_all(baseDir);
65+ std::filesystem::create_directories(modelPath.parent_path());
66+ std::ofstream(modelPath, std::ios::binary) << "unmanaged model contents";
67+ 
68+ const std::string configJson = R"({"models":[{"id":")" + modelId +
69+ R"(","name":"weights.bin","engine":")" + kEngineName +
70+ R"(","model_path":")" + modelId + R"(","base_url":"https://example.com/weights.bin",)"
71+ R"("size":24,"checksum":")" + Sha256(modelPath) + R"("}]})";
72+ ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
73+ ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
74+ ASSERT_EQ(GewuSmartServeRegisterEngine(kEngineName, MockAppleFmEngine::GetInterface), GEWU_SMARTSERVE_OK);
75+ ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
76+ 
77+ const std::string requestJson = R"({"model":")" + modelId +
78+ R"(","messages":[{"role":"user","content":"load"}]})";
79+ GewuSmartServeRequest request = nullptr;
80+ EXPECT_NE(GewuSmartServeChatCompletions(requestJson.c_str(), &request), GEWU_SMARTSERVE_OK);
81+ EXPECT_EQ(request, nullptr);
82+ EXPECT_EQ(GewuSmartServeUnregisterEngine(kEngineName), GEWU_SMARTSERVE_OK);
83+ std::filesystem::remove_all(baseDir);
84+}
85+ 
53TEST_F(GewuSmartServeApiTest, MultiFileDownloadReportsInstalledFileProgressProportionally)86TEST_F(GewuSmartServeApiTest, MultiFileDownloadReportsInstalledFileProgressProportionally)
54{87{
88+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
89+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
90+#endif
55 const std::string modelId = "multi-file-progress-model";91 const std::string modelId = "multi-file-progress-model";
56 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_multi_file_progress";92 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_multi_file_progress";
57 const auto modelDir = baseDir / "models";93 const auto modelDir = baseDir / "models";
@@ -79,14 +115,26 @@ TEST_F(GewuSmartServeApiTest, MultiFileDownloadReportsInstalledFileProgressPropo
79 ASSERT_TRUE(installed.is_open());115 ASSERT_TRUE(installed.is_open());
80 installed << std::string(kFileSize, 'a');116 installed << std::string(kFileSize, 'a');
81 }117 }
118+ json metadata;
119+ metadata["version"] = 1;
120+ metadata["models"] = json::object();
121+ metadata["models"][modelId] = {
122+ {"status", "partial"},
123+ {"path", std::filesystem::canonical(modelPath).string()},
124+ {"engine", "llamacpp"},
125+ {"verified", false},
126+ };
127+ std::ofstream(modelDir / "installed_models.json") << metadata.dump();
82 128 
83 const std::string configJson = R"({"models":[{"id":")" + modelId +129 const std::string configJson = R"({"models":[{"id":")" + modelId +
84 R"(","name":")" + modelId +130 R"(","name":")" + modelId +
85 R"(","engine":"llamacpp","model_path":")" + modelId +131 R"(","engine":"llamacpp","model_path":")" + modelId +
86- R"(","files":[{"filename":"a.bin","url":"file://)" + sourceA.string() +132+ R"(","files":[{"filename":"a.bin","url":"file+test://)" + sourceA.string() +
87 R"(","size":)" + std::to_string(kFileSize) +133 R"(","size":)" + std::to_string(kFileSize) +
88- R"(},{"filename":"b.bin","url":"file://)" + sourceB.string() +134+ R"(,"checksum":")" + Sha256(sourceA) +
89- R"(","size":)" + std::to_string(kFileSize) + R"(}]}]})";135+ R"("},{"filename":"b.bin","url":"file+test://)" + sourceB.string() +
136+ R"(","size":)" + std::to_string(kFileSize) +
137+ R"(,"checksum":")" + Sha256(sourceB) + R"("}]}]})";
90 138 
91 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);139 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
92 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);140 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
@@ -108,6 +156,9 @@ TEST_F(GewuSmartServeApiTest, MultiFileDownloadReportsInstalledFileProgressPropo
108 156 
109TEST_F(GewuSmartServeApiTest, SingleFileDownloadUsesNameInsideModelPathDirectory)157TEST_F(GewuSmartServeApiTest, SingleFileDownloadUsesNameInsideModelPathDirectory)
110{158{
159+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
160+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
161+#endif
111 const std::string modelId = "single-file-download-model";162 const std::string modelId = "single-file-download-model";
112 const std::string filename = "weights.gguf";163 const std::string filename = "weights.gguf";
113 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_single_file_path";164 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_single_file_path";
@@ -127,8 +178,9 @@ TEST_F(GewuSmartServeApiTest, SingleFileDownloadUsesNameInsideModelPathDirectory
127 const std::string configJson = R"({"models":[{"id":")" + modelId +178 const std::string configJson = R"({"models":[{"id":")" + modelId +
128 R"(","name":")" + filename +179 R"(","name":")" + filename +
129 R"(","engine":"llamacpp","model_path":")" + modelId +180 R"(","engine":"llamacpp","model_path":")" + modelId +
130- R"(","base_url":"file://)" + sourcePath.string() +181+ R"(","base_url":"file+test://)" + sourcePath.string() +
131- R"(","size":)" + std::to_string(kSourceSize) + R"(}]})";182+ R"(","size":)" + std::to_string(kSourceSize) +
183+ R"(,"checksum":")" + Sha256(sourcePath) + R"("}]})";
132 184 
133 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);185 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
134 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);186 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
@@ -144,6 +196,45 @@ TEST_F(GewuSmartServeApiTest, SingleFileDownloadUsesNameInsideModelPathDirectory
144 std::filesystem::remove_all(baseDir);196 std::filesystem::remove_all(baseDir);
145}197}
146 198 
199+// Verifies public downloads require SHA-256 and secure sources while accepting arbitrary valid HTTPS hosts.
200+TEST_F(GewuSmartServeApiTest, DownloadAcceptsCustomHttpsAndRejectsInsecureSources)
201+{
202+ const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_download_policy";
203+ const auto modelDir = baseDir / "models";
204+ std::filesystem::remove_all(baseDir);
205+ std::filesystem::create_directories(modelDir);
206+ ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
207+ std::ofstream(modelDir / "model.bin", std::ios::binary) << "x";
208+ 
209+ const std::string validChecksum = Sha256(modelDir / "model.bin");
210+ auto expectRejected = [&](const std::string& modelId, const std::string& url,
211+ const std::string& checksum) {
212+ const std::string configJson = R"({"models":[{"id":")" + modelId +
213+ R"(","name":"model.bin","engine":"llamacpp","base_url":")" + url +
214+ R"(","size":1,"checksum":")" + checksum + R"("}]})";
215+ EXPECT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
216+ EXPECT_EQ(GewuSmartServeDownloadModel(modelId.c_str(), nullptr, nullptr),
217+ GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
218+ };
219+ 
220+ expectRejected("missing-checksum-model",
221+ "https://modelscope.cn/models/MNN/Qwen3-0.6B-MNN/resolve/master/llm.mnn", "");
222+ expectRejected("cleartext-model", "http://example.com/model.bin", validChecksum);
223+ expectRejected("local-file-model", "file:///etc/passwd", validChecksum);
224+ 
225+ const std::string customModelId = "custom-https-model";
226+ const std::string customConfig = R"({"models":[{"id":")" + customModelId +
227+ R"(","name":"model.bin","engine":"llamacpp","base_url":"https://custom.example/model.bin",)" +
228+ R"("size":1,"checksum":")" + validChecksum + R"("}]})";
229+ ASSERT_EQ(GewuSmartServeSetModelsConfigJson(customConfig.c_str()), GEWU_SMARTSERVE_OK);
230+ EXPECT_EQ(GewuSmartServeDownloadModel(customModelId.c_str(), nullptr, nullptr), GEWU_SMARTSERVE_OK);
231+ EXPECT_FALSE(std::filesystem::exists(modelDir / "installed_models.json"));
232+ EXPECT_EQ(GewuSmartServeDeleteModel(customModelId.c_str()), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
233+ EXPECT_TRUE(std::filesystem::exists(modelDir / "model.bin"));
234+ 
235+ std::filesystem::remove_all(baseDir);
236+}
237+ 
147TEST_F(GewuSmartServeApiTest, SingleFileDirectoryPathIsNotReportedDownloaded)238TEST_F(GewuSmartServeApiTest, SingleFileDirectoryPathIsNotReportedDownloaded)
148{239{
149 const std::string modelId = "single-file-directory-status-model";240 const std::string modelId = "single-file-directory-status-model";
@@ -243,16 +334,17 @@ TEST_F(GewuSmartServeApiTest, DownloadRejectsPathTraversalInModelAndFilePaths)
243 334 
244 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);335 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
245 const std::string unsafeModelPathJson = R"({"models":[{"id":"unsafe-model-path",)"336 const std::string unsafeModelPathJson = R"({"models":[{"id":"unsafe-model-path",)"
246- R"("name":"../escape.bin","engine":"llamacpp","base_url":"file://)" +337+ R"("name":"../escape.bin","engine":"llamacpp","base_url":"https://example.com/model.bin",)"
247- sourcePath.string() + R"(","size":4}]})";338+ R"("size":4,"checksum":")" + Sha256(sourcePath) + R"("}]})";
248 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(unsafeModelPathJson.c_str()), GEWU_SMARTSERVE_OK);339 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(unsafeModelPathJson.c_str()), GEWU_SMARTSERVE_OK);
249 EXPECT_EQ(GewuSmartServeDownloadModel("unsafe-model-path", nullptr, nullptr),340 EXPECT_EQ(GewuSmartServeDownloadModel("unsafe-model-path", nullptr, nullptr),
250 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);341 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
251 EXPECT_FALSE(std::filesystem::exists(escapedPath));342 EXPECT_FALSE(std::filesystem::exists(escapedPath));
252 343 
253 const std::string unsafeFilePathJson = R"({"models":[{"id":"unsafe-file-path",)"344 const std::string unsafeFilePathJson = R"({"models":[{"id":"unsafe-file-path",)"
254- R"("name":"safe-dir","engine":"llamacpp","files":[{"filename":"../escape.bin","url":"file://)" +345+ R"("name":"safe-dir","engine":"llamacpp","files":[{"filename":"../escape.bin",)"
255- sourcePath.string() + R"(","size":4}]}]})";346+ R"("url":"https://example.com/model.bin","size":4,"checksum":")" +
347+ Sha256(sourcePath) + R"("}]}]})";
256 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(unsafeFilePathJson.c_str()), GEWU_SMARTSERVE_OK);348 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(unsafeFilePathJson.c_str()), GEWU_SMARTSERVE_OK);
257 EXPECT_EQ(GewuSmartServeDownloadModel("unsafe-file-path", nullptr, nullptr),349 EXPECT_EQ(GewuSmartServeDownloadModel("unsafe-file-path", nullptr, nullptr),
258 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);350 GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
@@ -291,7 +383,8 @@ TEST_F(GewuSmartServeApiTest, ServiceLoadModelsUsesSingleFileNameInsideModelPath
291 R"(","name":")" + filename +383 R"(","name":")" + filename +
292 R"(","engine":")" + std::string(kEngineName) +384 R"(","engine":")" + std::string(kEngineName) +
293 R"(","model_path":")" + modelId +385 R"(","model_path":")" + modelId +
294- R"(","base_url":"file://example"}]})";386+ R"(","base_url":"https://example.com/weights.gguf","checksum":")" + Sha256(filePath) +
387+ R"("}]})";
295 388 
296 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);389 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
297 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);390 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
@@ -395,6 +488,9 @@ TEST_F(GewuSmartServeApiTest, ServiceLoadModelsRejectsEscapedConfiguredPaths)
395 488 
396TEST_F(GewuSmartServeApiTest, DownloadPersistsMetadataAndDeleteCleansIt)489TEST_F(GewuSmartServeApiTest, DownloadPersistsMetadataAndDeleteCleansIt)
397{490{
491+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
492+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
493+#endif
398 const std::string modelId = "metadata-model";494 const std::string modelId = "metadata-model";
399 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_metadata";495 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_metadata";
400 const auto modelDir = baseDir / "models";496 const auto modelDir = baseDir / "models";
@@ -412,8 +508,9 @@ TEST_F(GewuSmartServeApiTest, DownloadPersistsMetadataAndDeleteCleansIt)
412 }508 }
413 509 
414 const std::string configJson = R"({"models":[{"id":")" + modelId +510 const std::string configJson = R"({"models":[{"id":")" + modelId +
415- R"(","name":"downloaded.bin","engine":"llamacpp","base_url":"file://)" +511+ R"(","name":"downloaded.bin","engine":"llamacpp","base_url":"file+test://)" +
416- sourcePath.string() + R"(","size":)" + std::to_string(kSourceSize) + R"(}]})";512+ sourcePath.string() + R"(","size":)" + std::to_string(kSourceSize) +
513+ R"(,"checksum":")" + Sha256(sourcePath) + R"("}]})";
417 514 
418 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);515 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
419 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);516 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
@@ -452,6 +549,9 @@ TEST_F(GewuSmartServeApiTest, DownloadPersistsMetadataAndDeleteCleansIt)
452 549 
453TEST_F(GewuSmartServeApiTest, PauseDownloadCancelsAndNextDownloadResumes)550TEST_F(GewuSmartServeApiTest, PauseDownloadCancelsAndNextDownloadResumes)
454{551{
552+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
553+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
554+#endif
455 const std::string modelId = "pause-resume-model";555 const std::string modelId = "pause-resume-model";
456 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_pause_resume";556 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_pause_resume";
457 const auto sourcePath = baseDir / "source.bin";557 const auto sourcePath = baseDir / "source.bin";
@@ -474,9 +574,9 @@ TEST_F(GewuSmartServeApiTest, PauseDownloadCancelsAndNextDownloadResumes)
474 }574 }
475 575 
476 const std::string configJson = R"({"models":[{"id":")" + modelId +576 const std::string configJson = R"({"models":[{"id":")" + modelId +
477- R"(","name":"downloaded.bin","engine":"llamacpp","base_url":"file://)" +577+ R"(","name":"downloaded.bin","engine":"llamacpp","base_url":"file+test://)" +
478 sourcePath.string() + R"(","model_path":")" + modelId + R"(","size":)" +578 sourcePath.string() + R"(","model_path":")" + modelId + R"(","size":)" +
479- std::to_string(kSourceSize) + R"(}]})";579+ std::to_string(kSourceSize) + R"(,"checksum":")" + Sha256(sourcePath) + R"("}]})";
480 580 
481 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);581 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
482 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);582 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
@@ -74,6 +74,9 @@ void ReenterLifecycleApisFromProgress(
74// Verifies callbacks allow status queries but reject lifecycle operations that can wait on the active download.74// Verifies callbacks allow status queries but reject lifecycle operations that can wait on the active download.
75TEST_F(GewuSmartServeApiTest, ProgressCallbackRejectsBlockingLifecycleReentryWithoutDeadlock)75TEST_F(GewuSmartServeApiTest, ProgressCallbackRejectsBlockingLifecycleReentryWithoutDeadlock)
76{76{
77+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
78+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
79+#endif
77 const std::string modelId = "progress-reentry-model";80 const std::string modelId = "progress-reentry-model";
78 const auto baseDir = std::filesystem::temp_directory_path() /81 const auto baseDir = std::filesystem::temp_directory_path() /
79 "gewu_smartserve_progress_reentry";82 "gewu_smartserve_progress_reentry";
@@ -104,6 +107,9 @@ TEST_F(GewuSmartServeApiTest, ProgressCallbackRejectsBlockingLifecycleReentryWit
104 107 
105TEST_F(GewuSmartServeApiTest, ThreeConcurrentDownloadsOfSameModelAreIdempotent)108TEST_F(GewuSmartServeApiTest, ThreeConcurrentDownloadsOfSameModelAreIdempotent)
106{109{
110+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
111+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
112+#endif
107 const std::string modelId = "concurrent-download-model";113 const std::string modelId = "concurrent-download-model";
108 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_concurrent_download";114 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_concurrent_download";
109 const auto sourcePath = baseDir / "source.bin";115 const auto sourcePath = baseDir / "source.bin";
@@ -124,7 +130,11 @@ TEST_F(GewuSmartServeApiTest, ThreeConcurrentDownloadsOfSameModelAreIdempotent)
124 first = std::async(std::launch::async, [&] {130 first = std::async(std::launch::async, [&] {
125 return GewuSmartServeDownloadModel(modelId.c_str(), BlockingDownloadProgress::Callback, &progress);131 return GewuSmartServeDownloadModel(modelId.c_str(), BlockingDownloadProgress::Callback, &progress);
126 });132 });
127- ASSERT_TRUE(progress.WaitUntilBlocked()) << "first download did not reach the progress gate";133+ const bool firstDownloadBlocked = progress.WaitUntilBlocked();
134+ if (!firstDownloadBlocked) {
135+ progress.Release();
136+ }
137+ ASSERT_TRUE(firstDownloadBlocked) << "first download did not reach the progress gate";
128 138 
129 auto second = std::async(std::launch::async, [&] {139 auto second = std::async(std::launch::async, [&] {
130 return GewuSmartServeDownloadModel(modelId.c_str(), nullptr, nullptr);140 return GewuSmartServeDownloadModel(modelId.c_str(), nullptr, nullptr);
@@ -147,6 +157,9 @@ TEST_F(GewuSmartServeApiTest, ThreeConcurrentDownloadsOfSameModelAreIdempotent)
147 157 
148TEST_F(GewuSmartServeApiTest, DeleteDuringDownloadCancelsAndRemovesPartialModel)158TEST_F(GewuSmartServeApiTest, DeleteDuringDownloadCancelsAndRemovesPartialModel)
149{159{
160+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
161+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
162+#endif
150 const std::string modelId = "delete-during-download-model";163 const std::string modelId = "delete-during-download-model";
151 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_delete_during_download";164 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_delete_during_download";
152 const auto sourcePath = baseDir / "source.bin";165 const auto sourcePath = baseDir / "source.bin";
@@ -168,7 +181,11 @@ TEST_F(GewuSmartServeApiTest, DeleteDuringDownloadCancelsAndRemovesPartialModel)
168 download = std::async(std::launch::async, [&] {181 download = std::async(std::launch::async, [&] {
169 return GewuSmartServeDownloadModel(modelId.c_str(), BlockingDownloadProgress::Callback, &progress);182 return GewuSmartServeDownloadModel(modelId.c_str(), BlockingDownloadProgress::Callback, &progress);
170 });183 });
171- ASSERT_TRUE(progress.WaitUntilBlocked()) << "download did not reach the progress gate";184+ const bool downloadBlocked = progress.WaitUntilBlocked();
185+ if (!downloadBlocked) {
186+ progress.Release();
187+ }
188+ ASSERT_TRUE(downloadBlocked) << "download did not reach the progress gate";
172 189 
173 auto remove = std::async(std::launch::async, [&] {190 auto remove = std::async(std::launch::async, [&] {
174 return GewuSmartServeDeleteModel(modelId.c_str());191 return GewuSmartServeDeleteModel(modelId.c_str());
@@ -188,6 +205,9 @@ TEST_F(GewuSmartServeApiTest, DeleteDuringDownloadCancelsAndRemovesPartialModel)
188 205 
189TEST_F(GewuSmartServeApiTest, DownloadStartedDuringDeleteWaitsAndRestoresModel)206TEST_F(GewuSmartServeApiTest, DownloadStartedDuringDeleteWaitsAndRestoresModel)
190{207{
208+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
209+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
210+#endif
191 const std::string modelId = "download-during-delete-model";211 const std::string modelId = "download-during-delete-model";
192 const std::string engineName = "download-during-delete-engine";212 const std::string engineName = "download-during-delete-engine";
193 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_download_during_delete";213 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_download_during_delete";
@@ -230,6 +250,9 @@ TEST_F(GewuSmartServeApiTest, DownloadStartedDuringDeleteWaitsAndRestoresModel)
230 250 
231TEST_F(GewuSmartServeApiTest, InferenceStartedDuringDeleteIsRejected)251TEST_F(GewuSmartServeApiTest, InferenceStartedDuringDeleteIsRejected)
232{252{
253+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
254+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
255+#endif
233 const std::string modelId = "inference-during-delete-model";256 const std::string modelId = "inference-during-delete-model";
234 const std::string engineName = "inference-during-delete-engine";257 const std::string engineName = "inference-during-delete-engine";
235 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_inference_during_delete";258 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_inference_during_delete";
@@ -278,6 +301,9 @@ TEST_F(GewuSmartServeApiTest, InferenceStartedDuringDeleteIsRejected)
278 301 
279TEST_F(GewuSmartServeApiTest, ConcurrentRepeatedDeleteHasDeterministicNotFoundResult)302TEST_F(GewuSmartServeApiTest, ConcurrentRepeatedDeleteHasDeterministicNotFoundResult)
280{303{
304+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
305+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
306+#endif
281 const std::string modelId = "repeated-delete-model";307 const std::string modelId = "repeated-delete-model";
282 const std::string engineName = "repeated-delete-engine";308 const std::string engineName = "repeated-delete-engine";
283 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_repeated_delete";309 const auto baseDir = std::filesystem::temp_directory_path() / "gewu_smartserve_repeated_delete";
@@ -418,6 +444,16 @@ TEST_F(GewuSmartServeApiTest, DeletingLargeModelDoesNotBlockUnrelatedEngineRegis
418 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);444 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
419 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);445 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
420 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);446 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
447+ json metadata;
448+ metadata["version"] = 1;
449+ metadata["models"] = json::object();
450+ metadata["models"][modelId] = {
451+ {"status", "partial"},
452+ {"path", std::filesystem::canonical(modelPath).string()},
453+ {"engine", "llamacpp"},
454+ {"verified", false},
455+ };
456+ std::ofstream(modelDir / "installed_models.json") << metadata.dump();
421 457 
422 auto deletion = std::async(std::launch::async, [&] {458 auto deletion = std::async(std::launch::async, [&] {
423 return GewuSmartServeDeleteModel(modelId.c_str());459 return GewuSmartServeDeleteModel(modelId.c_str());
@@ -428,21 +464,28 @@ TEST_F(GewuSmartServeApiTest, DeletingLargeModelDoesNotBlockUnrelatedEngineRegis
428 removalStarted = std::any_of(files.begin(), files.end(), [](const auto& file) {464 removalStarted = std::any_of(files.begin(), files.end(), [](const auto& file) {
429 return !std::filesystem::exists(file);465 return !std::filesystem::exists(file);
430 });466 });
431- if (removalStarted ||467+ if (removalStarted) {
432- deletion.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) {468+ break;
469+ }
470+ if (deletion.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) {
471+ removalStarted = !std::filesystem::exists(modelPath);
433 break;472 break;
434 }473 }
435 std::this_thread::yield();474 std::this_thread::yield();
436 }475 }
437- ASSERT_TRUE(removalStarted) << "model deletion completed before its filesystem phase was observed";476+ ASSERT_TRUE(removalStarted) << "did not observe the file deletion phase";
477+ const bool deletionWasInFlight =
478+ deletion.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout;
438 479 
439 auto registration = std::async(std::launch::async, [&] {480 auto registration = std::async(std::launch::async, [&] {
440 return GewuSmartServeRegisterEngine(engineName.c_str(), MockAppleFmEngine::GetInterface);481 return GewuSmartServeRegisterEngine(engineName.c_str(), MockAppleFmEngine::GetInterface);
441 });482 });
442 ASSERT_EQ(registration.wait_for(std::chrono::seconds(5)), std::future_status::ready);483 ASSERT_EQ(registration.wait_for(std::chrono::seconds(5)), std::future_status::ready);
443 EXPECT_EQ(registration.get(), GEWU_SMARTSERVE_OK);484 EXPECT_EQ(registration.get(), GEWU_SMARTSERVE_OK);
444- EXPECT_EQ(deletion.wait_for(std::chrono::milliseconds(0)), std::future_status::timeout)485+ if (deletionWasInFlight) {
445- << "engine registration only completed after recursive deletion released the management lock";486+ EXPECT_EQ(deletion.wait_for(std::chrono::milliseconds(0)), std::future_status::timeout)
487+ << "engine registration waited until recursive deletion released the management lock";
488+ }
446 EXPECT_EQ(deletion.get(), GEWU_SMARTSERVE_OK);489 EXPECT_EQ(deletion.get(), GEWU_SMARTSERVE_OK);
447 EXPECT_EQ(GewuSmartServeUnregisterEngine(engineName.c_str()), GEWU_SMARTSERVE_OK);490 EXPECT_EQ(GewuSmartServeUnregisterEngine(engineName.c_str()), GEWU_SMARTSERVE_OK);
448 std::filesystem::remove_all(baseDir);491 std::filesystem::remove_all(baseDir);
@@ -490,6 +533,9 @@ TEST_F(GewuSmartServeApiTest, LoadModelsSkipsModelWhileItsFilesAreBeingDeleted)
490// Verifies delete unloads a runtime model after its files and metadata disappear.533// Verifies delete unloads a runtime model after its files and metadata disappear.
491TEST_F(GewuSmartServeApiTest, DeleteUnloadsRuntimeModelWhenFilesAndMetadataAreAlreadyMissing)534TEST_F(GewuSmartServeApiTest, DeleteUnloadsRuntimeModelWhenFilesAndMetadataAreAlreadyMissing)
492{535{
536+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
537+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
538+#endif
493 const std::string modelId = "delete-loaded-missing-files";539 const std::string modelId = "delete-loaded-missing-files";
494 const std::string engineName = "delete-loaded-missing-files-engine";540 const std::string engineName = "delete-loaded-missing-files-engine";
495 const auto baseDir = std::filesystem::temp_directory_path() /541 const auto baseDir = std::filesystem::temp_directory_path() /
@@ -505,8 +551,9 @@ TEST_F(GewuSmartServeApiTest, DeleteUnloadsRuntimeModelWhenFilesAndMetadataAreAl
505 const std::string configJson = R"({"models":[{"id":")" + modelId +551 const std::string configJson = R"({"models":[{"id":")" + modelId +
506 R"(","name":")" + modelId + R"(","engine":")" + engineName +552 R"(","name":")" + modelId + R"(","engine":")" + engineName +
507 R"(","model_path":")" + modelId +553 R"(","model_path":")" + modelId +
508- R"(","files":[{"filename":"weights.bin","url":"file://)" +554+ R"(","files":[{"filename":"weights.bin","url":"file+test://)" +
509- sourcePath.string() + R"("}]}]})";555+ sourcePath.string() + R"(","size":5,"checksum":")" + Sha256(sourcePath) +
556+ R"("}]}]})";
510 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);557 ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
511 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);558 ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
512 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);559 ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
@@ -539,4 +586,112 @@ TEST_F(GewuSmartServeApiTest, DeleteUnloadsRuntimeModelWhenFilesAndMetadataAreAl
539 std::filesystem::remove_all(baseDir);586 std::filesystem::remove_all(baseDir);
540}587}
541 588 
589+// Verifies a preinstalled model is accepted only when its configured SHA-256 matches.
590+TEST_F(GewuSmartServeApiTest, PreinstalledModelRequiresMatchingChecksum)
591+{
592+ const std::string modelId = "preinstalled-integrity-model";
593+ const std::string engineName = "preinstalled-integrity-engine";
594+ const auto baseDir = std::filesystem::temp_directory_path() /
595+ "gewu_smartserve_preinstalled_integrity";
596+ const auto modelDir = baseDir / "models";
597+ const auto modelPath = modelDir / modelId / "weights.bin";
598+ std::filesystem::remove_all(baseDir);
599+ std::filesystem::create_directories(modelPath.parent_path());
600+ std::ofstream(modelPath, std::ios::binary) << "preinstalled-model";
601+ 
602+ const std::string checksum = Sha256(modelPath);
603+ const std::string configJson = R"({"models":[{"id":")" + modelId +
604+ R"(","name":")" + modelId + R"(","engine":")" + engineName +
605+ R"(","model_path":")" + modelId +
606+ R"(","files":[{"filename":"weights.bin","checksum":")" + checksum + R"("}]}]})";
607+ ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
608+ ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
609+ ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
610+ ASSERT_EQ(GewuSmartServeRegisterEngine(engineName.c_str(), MockAppleFmEngine::GetInterface),
611+ GEWU_SMARTSERVE_OK);
612+ 
613+ CompleteMockChat(modelId);
614+ EXPECT_EQ(GewuSmartServeUnloadModel(modelId.c_str()), GEWU_SMARTSERVE_OK);
615+ EXPECT_EQ(GewuSmartServeUnregisterEngine(engineName.c_str()), GEWU_SMARTSERVE_OK);
616+ std::filesystem::remove_all(baseDir);
617+}
618+ 
619+// Verifies unchecksummed preinstalled files never reach an inference engine, including test builds.
620+TEST_F(GewuSmartServeApiTest, PreinstalledModelWithoutChecksumIsRejected)
621+{
622+ const std::string modelId = "unverified-preinstalled-model";
623+ const std::string engineName = "unverified-preinstalled-engine";
624+ const auto baseDir = std::filesystem::temp_directory_path() /
625+ "gewu_smartserve_unverified_preinstalled";
626+ const auto modelDir = baseDir / "models";
627+ const auto modelPath = modelDir / modelId / "weights.bin";
628+ std::filesystem::remove_all(baseDir);
629+ std::filesystem::create_directories(modelPath.parent_path());
630+ std::ofstream(modelPath, std::ios::binary) << "preinstalled-model";
631+ 
632+ const std::string configJson = R"({"models":[{"id":")" + modelId +
633+ R"(","name":")" + modelId + R"(","engine":")" + engineName +
634+ R"(","model_path":")" + modelId +
635+ R"(","files":[{"filename":"weights.bin"}]}]})";
636+ ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
637+ ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
638+ ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
639+ ASSERT_EQ(GewuSmartServeRegisterEngine(engineName.c_str(), MockAppleFmEngine::GetInterface),
640+ GEWU_SMARTSERVE_OK);
641+ 
642+ const std::string requestJson = R"({"model":")" + modelId +
643+ R"(","messages":[{"role":"user","content":"integrity"}]})";
644+ GewuSmartServeRequest request = nullptr;
645+ EXPECT_NE(GewuSmartServeChatCompletions(requestJson.c_str(), &request), GEWU_SMARTSERVE_OK);
646+ EXPECT_EQ(request, nullptr);
647+ EXPECT_TRUE(MockAppleFmEngine::LastCreateModelJson().empty());
648+ 
649+ EXPECT_EQ(GewuSmartServeUnregisterEngine(engineName.c_str()), GEWU_SMARTSERVE_OK);
650+ std::filesystem::remove_all(baseDir);
651+}
652+ 
653+#if !defined(_WIN32)
654+// Verifies a registered model cannot delete files reached through a symlink escaping the model root.
655+TEST_F(GewuSmartServeApiTest, DeleteRejectsIntermediateSymlinkEscapingModelRoot)
656+{
657+ const std::string modelId = "symlink-escape-model";
658+ const auto baseDir = std::filesystem::temp_directory_path() /
659+ "gewu_smartserve_symlink_delete";
660+ const auto modelDir = baseDir / "models";
661+ const auto outsideDir = baseDir / "outside";
662+ const auto victimDir = outsideDir / "victim";
663+ const auto markerPath = victimDir / "keep.marker";
664+ const auto modelPath = victimDir / "weights.bin";
665+ const auto metadataPath = modelDir / "installed_models.json";
666+ std::filesystem::remove_all(baseDir);
667+ std::filesystem::create_directories(victimDir);
668+ std::ofstream(markerPath, std::ios::binary) << "keep";
669+ std::ofstream(modelPath, std::ios::binary) << "model";
670+ std::filesystem::create_directories(modelDir);
671+ std::filesystem::create_directory_symlink(outsideDir, modelDir / "link");
672+ 
673+ const std::string configJson = R"({"models":[{"id":")" + modelId +
674+ R"(","name":"weights.bin","engine":"llamacpp","model_path":"link/victim",)"
675+ R"("base_url":"https://example.com/weights.bin","size":5}]})";
676+ ASSERT_EQ(GewuSmartServeSetModelsDirectory(modelDir.string().c_str()), GEWU_SMARTSERVE_OK);
677+ ASSERT_EQ(GewuSmartServeInitialize(), GEWU_SMARTSERVE_OK);
678+ ASSERT_EQ(GewuSmartServeSetModelsConfigJson(configJson.c_str()), GEWU_SMARTSERVE_OK);
679+ json metadata;
680+ metadata["version"] = 1;
681+ metadata["models"] = json::object();
682+ metadata["models"][modelId] = {
683+ {"status", "downloaded"},
684+ {"path", (std::filesystem::weakly_canonical(modelDir) / "link" / "victim" / "weights.bin").string()},
685+ {"engine", "llamacpp"},
686+ {"verified", true},
687+ };
688+ std::ofstream(metadataPath) << metadata.dump();
689+ 
690+ EXPECT_EQ(GewuSmartServeDeleteModel(modelId.c_str()), GEWU_SMARTSERVE_ERROR_INVALID_ARGUMENT);
691+ EXPECT_TRUE(std::filesystem::exists(markerPath));
692+ EXPECT_TRUE(std::filesystem::exists(modelPath));
693+ std::filesystem::remove_all(baseDir);
694+}
695+#endif
696+ 
542} // namespace697} // namespace
@@ -35,6 +35,7 @@
35#include "model/model_downloader.h"35#include "model/model_downloader.h"
36#include "model/model_config.h"36#include "model/model_config.h"
37#include "model/model_file_manager.h"37#include "model/model_file_manager.h"
38+#include "model/model_util.h"
38#include "service/service.h"39#include "service/service.h"
39#include "smart_serve_client.h"40#include "smart_serve_client.h"
40 41 
@@ -171,6 +172,7 @@ TEST_F(ModelDownloaderTest, ModelReadinessChecksumCacheInvalidatesWhenFileChange
171 172 
172 const std::string originalHash = ModelDownloader::CalculateSHA256(testFile.string());173 const std::string originalHash = ModelDownloader::CalculateSHA256(testFile.string());
173 auto& client = SmartServeClient::GetInstance();174 auto& client = SmartServeClient::GetInstance();
175+ EXPECT_FALSE(client.IsModelReady(testFile.string(), ""));
174 ASSERT_TRUE(client.IsModelReady(testFile.string(), originalHash));176 ASSERT_TRUE(client.IsModelReady(testFile.string(), originalHash));
175 EXPECT_TRUE(client.IsModelReady(testFile.string(), originalHash));177 EXPECT_TRUE(client.IsModelReady(testFile.string(), originalHash));
176 178 
@@ -221,6 +223,9 @@ TEST_F(ModelDownloaderTest, ChecksumWithSha256Prefix)
221 223 
222TEST_F(ModelDownloaderTest, SameSizeChecksumMismatchIsDownloadedAgainAndMadeReadOnly)224TEST_F(ModelDownloaderTest, SameSizeChecksumMismatchIsDownloadedAgainAndMadeReadOnly)
223{225{
226+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
227+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
228+#endif
224 const auto baseDir = std::filesystem::temp_directory_path() /229 const auto baseDir = std::filesystem::temp_directory_path() /
225 "smartserve_same_size_checksum_redownload";230 "smartserve_same_size_checksum_redownload";
226 const auto source = baseDir / "source.bin";231 const auto source = baseDir / "source.bin";
@@ -243,7 +248,7 @@ TEST_F(ModelDownloaderTest, SameSizeChecksumMismatchIsDownloadedAgainAndMadeRead
243 248 
244 const std::string checksum = ModelDownloader::CalculateSHA256(source.string());249 const std::string checksum = ModelDownloader::CalculateSHA256(source.string());
245 ModelDownloader downloader(250 ModelDownloader downloader(
246- "file://" + source.string(), destination.string(), expectedContent.size(), checksum);251+ "file+test://" + source.string(), destination.string(), expectedContent.size(), checksum);
247 ASSERT_EQ(downloader.Download(), DownloadError::OK);252 ASSERT_EQ(downloader.Download(), DownloadError::OK);
248 253 
249 std::ifstream downloaded(destination, std::ios::binary);254 std::ifstream downloaded(destination, std::ios::binary);
@@ -261,6 +266,9 @@ TEST_F(ModelDownloaderTest, SameSizeChecksumMismatchIsDownloadedAgainAndMadeRead
261 266 
262TEST_F(ModelDownloaderTest, ReadOnlyOlderVersionIsReplacedWhenNewVersionIsLarger)267TEST_F(ModelDownloaderTest, ReadOnlyOlderVersionIsReplacedWhenNewVersionIsLarger)
263{268{
269+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
270+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
271+#endif
264 const auto baseDir = std::filesystem::temp_directory_path() /272 const auto baseDir = std::filesystem::temp_directory_path() /
265 "smartserve_read_only_model_upgrade";273 "smartserve_read_only_model_upgrade";
266 const auto source = baseDir / "source.bin";274 const auto source = baseDir / "source.bin";
@@ -286,7 +294,7 @@ TEST_F(ModelDownloaderTest, ReadOnlyOlderVersionIsReplacedWhenNewVersionIsLarger
286 294 
287 const std::string checksum = ModelDownloader::CalculateSHA256(source.string());295 const std::string checksum = ModelDownloader::CalculateSHA256(source.string());
288 ModelDownloader downloader(296 ModelDownloader downloader(
289- "file://" + source.string(), destination.string(), expectedContent.size(), checksum);297+ "file+test://" + source.string(), destination.string(), expectedContent.size(), checksum);
290 ASSERT_EQ(downloader.Download(), DownloadError::OK);298 ASSERT_EQ(downloader.Download(), DownloadError::OK);
291 299 
292 std::ifstream downloaded(destination, std::ios::binary);300 std::ifstream downloaded(destination, std::ios::binary);
@@ -302,6 +310,9 @@ TEST_F(ModelDownloaderTest, ReadOnlyOlderVersionIsReplacedWhenNewVersionIsLarger
302 310 
303TEST_F(ModelDownloaderTest, ModelFileManagerDownloadsMultipleFiles)311TEST_F(ModelDownloaderTest, ModelFileManagerDownloadsMultipleFiles)
304{312{
313+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
314+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
315+#endif
305 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_test";316 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_test";
306 const auto sourceA = baseDir / "source_a.bin";317 const auto sourceA = baseDir / "source_a.bin";
307 const auto sourceB = baseDir / "source_b.bin";318 const auto sourceB = baseDir / "source_b.bin";
@@ -323,8 +334,10 @@ TEST_F(ModelDownloaderTest, ModelFileManagerDownloadsMultipleFiles)
323 334 
324 ModelFileManager::Instance().SetMaxConcurrentDownloads(2);335 ModelFileManager::Instance().SetMaxConcurrentDownloads(2);
325 std::vector<ModelFileManager::FileDownloadSpec> files = {336 std::vector<ModelFileManager::FileDownloadSpec> files = {
326- {"file://" + sourceA.string(), destA.string(), 1024 * 64, ""},337+ {"file+test://" + sourceA.string(), destA.string(), 1024 * 64,
327- {"file://" + sourceB.string(), destB.string(), 1024 * 64, ""},338+ ModelDownloader::CalculateSHA256(sourceA.string())},
339+ {"file+test://" + sourceB.string(), destB.string(), 1024 * 64,
340+ ModelDownloader::CalculateSHA256(sourceB.string())},
328 };341 };
329 int progressCalls = 0;342 int progressCalls = 0;
330 bool callbackCanReenterManager = false;343 bool callbackCanReenterManager = false;
@@ -371,6 +384,9 @@ TEST_F(ModelDownloaderTest, ModelFileManagerDownloadsMultipleFiles)
371 384 
372TEST_F(ModelDownloaderTest, ModelFileManagerEmitsFinalProgressForAlreadyCompleteFile)385TEST_F(ModelDownloaderTest, ModelFileManagerEmitsFinalProgressForAlreadyCompleteFile)
373{386{
387+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
388+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
389+#endif
374 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_complete_progress";390 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_complete_progress";
375 const auto source = baseDir / "source.bin";391 const auto source = baseDir / "source.bin";
376 const auto dest = baseDir / "out" / "complete.bin";392 const auto dest = baseDir / "out" / "complete.bin";
@@ -394,7 +410,8 @@ TEST_F(ModelDownloaderTest, ModelFileManagerEmitsFinalProgressForAlreadyComplete
394 float lastProgress = 0.0f;410 float lastProgress = 0.0f;
395 const auto err = ModelFileManager::Instance().DownloadModelFiles(411 const auto err = ModelFileManager::Instance().DownloadModelFiles(
396 "manager-complete-progress-test",412 "manager-complete-progress-test",
397- {{"file://" + source.string(), dest.string(), 1024 * 32, ""}},413+ {{"file+test://" + source.string(), dest.string(), 1024 * 32,
414+ ModelDownloader::CalculateSHA256(dest.string())}},
398 [&](const std::string&, int64_t downloaded, int64_t total, float progress) {415 [&](const std::string&, int64_t downloaded, int64_t total, float progress) {
399 progressCalls++;416 progressCalls++;
400 lastDownloaded = downloaded;417 lastDownloaded = downloaded;
@@ -419,8 +436,9 @@ TEST_F(ModelDownloaderTest, ModelFileManagerRejectsUnrepresentableAggregateSize)
419 "smartserve_model_file_manager_size_overflow";436 "smartserve_model_file_manager_size_overflow";
420 const std::vector<ModelFileManager::FileDownloadSpec> files = {437 const std::vector<ModelFileManager::FileDownloadSpec> files = {
421 {"https://example.com/a.bin", (modelRoot / "a.bin").string(),438 {"https://example.com/a.bin", (modelRoot / "a.bin").string(),
422- std::numeric_limits<int64_t>::max(), ""},439+ std::numeric_limits<int64_t>::max(), std::string(64, 'a')},
423- {"https://example.com/b.bin", (modelRoot / "b.bin").string(), 1, ""},440+ {"https://example.com/b.bin", (modelRoot / "b.bin").string(), 1,
441+ std::string(64, 'a')},
424 };442 };
425 443 
426 EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(444 EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
@@ -428,6 +446,32 @@ TEST_F(ModelDownloaderTest, ModelFileManagerRejectsUnrepresentableAggregateSize)
428 DownloadError::INVALID_ARGUMENT);446 DownloadError::INVALID_ARGUMENT);
429}447}
430 448 
449+// Verifies an equal-sized cached model is checksummed and replaced when its contents are stale.
450+TEST_F(ModelDownloaderTest, ModelFileManagerReverifiesSameSizeCachedFile)
451+{
452+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
453+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
454+#endif
455+ const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_cache_reverify_test";
456+ const auto source = baseDir / "source.bin";
457+ const auto destination = baseDir / "destination.bin";
458+ std::filesystem::remove_all(baseDir);
459+ std::filesystem::create_directories(baseDir);
460+ std::ofstream(source, std::ios::binary) << "trusted-content";
461+ std::ofstream(destination, std::ios::binary) << "tampered-data!!";
462+ ASSERT_EQ(std::filesystem::file_size(source), std::filesystem::file_size(destination));
463+ const std::string checksum = ModelDownloader::CalculateSHA256(source.string());
464+ 
465+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
466+ "cache-reverify-test",
467+ {{"file+test://" + source.string(), destination.string(),
468+ static_cast<int64_t>(std::filesystem::file_size(source)), checksum}},
469+ nullptr, false, baseDir.string()), DownloadError::OK);
470+ EXPECT_EQ(ModelDownloader::CalculateSHA256(destination.string()), checksum);
471+ 
472+ std::filesystem::remove_all(baseDir);
473+}
474+ 
431TEST_F(ModelDownloaderTest, ModelFileManagerRejectsDestinationOutsideStorageRoot)475TEST_F(ModelDownloaderTest, ModelFileManagerRejectsDestinationOutsideStorageRoot)
432{476{
433 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_path_guard";477 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_path_guard";
@@ -445,7 +489,8 @@ TEST_F(ModelDownloaderTest, ModelFileManagerRejectsDestinationOutsideStorageRoot
445 489 
446 const auto err = ModelFileManager::Instance().DownloadModelFiles(490 const auto err = ModelFileManager::Instance().DownloadModelFiles(
447 "manager-path-guard-test",491 "manager-path-guard-test",
448- {{"file://" + source.string(), escapedDest.string(), 5, ""}},492+ {{"https://example.com/model.bin", escapedDest.string(), 5,
493+ ModelDownloader::CalculateSHA256(source.string())}},
449 ModelFileManager::ProgressCallback{},494 ModelFileManager::ProgressCallback{},
450 false,495 false,
451 modelRoot.string());496 modelRoot.string());
@@ -456,6 +501,132 @@ TEST_F(ModelDownloaderTest, ModelFileManagerRejectsDestinationOutsideStorageRoot
456 std::filesystem::remove_all(baseDir);501 std::filesystem::remove_all(baseDir);
457}502}
458 503 
504+#if !defined(_WIN32)
505+// Verifies the worker opens every managed destination below a no-follow directory descriptor.
506+TEST_F(ModelDownloaderTest, ModelFileManagerRejectsSymlinkedDestinationAncestor)
507+{
508+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
509+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
510+#endif
511+ const auto baseDir = std::filesystem::temp_directory_path() /
512+ "smartserve_model_file_manager_symlink_destination";
513+ const auto source = baseDir / "source.bin";
514+ const auto modelRoot = baseDir / "models";
515+ const auto outsideDir = baseDir / "outside";
516+ const auto escapedDestination = modelRoot / "linked" / "weights.bin";
517+ std::filesystem::remove_all(baseDir);
518+ std::filesystem::create_directories(modelRoot);
519+ std::filesystem::create_directories(outsideDir);
520+ std::ofstream(source, std::ios::binary) << "trusted-content";
521+ std::filesystem::create_directory_symlink(outsideDir, modelRoot / "linked");
522+ 
523+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
524+ "manager-symlink-destination-test",
525+ {{"file+test://" + source.string(), escapedDestination.string(), 15,
526+ ModelDownloader::CalculateSHA256(source.string())}},
527+ nullptr, false, modelRoot.string()), DownloadError::FILE_ERROR);
528+ EXPECT_FALSE(std::filesystem::exists(outsideDir / "weights.bin"));
529+ 
530+ std::filesystem::remove_all(baseDir);
531+}
532+ 
533+// Verifies secure recursive removal refuses a symlink introduced below the managed root.
534+TEST_F(ModelDownloaderTest, SecureManagedRemovalRejectsSymlinkedAncestor)
535+{
536+ const auto baseDir = std::filesystem::temp_directory_path() /
537+ "smartserve_secure_managed_removal";
538+ const auto modelRoot = baseDir / "models";
539+ const auto outsideDir = baseDir / "outside";
540+ const auto victimDir = outsideDir / "victim";
541+ const auto marker = victimDir / "keep.marker";
542+ std::filesystem::remove_all(baseDir);
543+ std::filesystem::create_directories(victimDir);
544+ std::ofstream(marker, std::ios::binary) << "keep";
545+ std::filesystem::create_directories(modelRoot);
546+ std::filesystem::create_directory_symlink(outsideDir, modelRoot / "linked");
547+ 
548+ EXPECT_FALSE(RemoveManagedPathNoFollow(
549+ modelRoot.string(), (modelRoot / "linked" / "victim").string()));
550+ EXPECT_TRUE(std::filesystem::exists(marker));
551+ 
552+ std::filesystem::remove_all(baseDir);
553+}
554+#endif
555+ 
556+// Verifies model file downloads reject missing checksums, malformed checksums, and insecure sources.
557+TEST_F(ModelDownloaderTest, ModelFileManagerRejectsInsecureSourcesAndInvalidChecksums)
558+{
559+ const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_download_policy_test";
560+ const auto source = baseDir / "source.bin";
561+ const auto destination = baseDir / "destination.bin";
562+ std::filesystem::remove_all(baseDir);
563+ std::filesystem::create_directories(baseDir);
564+ std::ofstream(source, std::ios::binary) << "trusted-content";
565+ const std::string checksum = ModelDownloader::CalculateSHA256(source.string());
566+ 
567+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
568+ "missing-checksum", {{"https://example.com/model.bin", destination.string(), 15, ""}},
569+ nullptr, false, baseDir.string()), DownloadError::INVALID_ARGUMENT);
570+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
571+ "malformed-checksum", {{"https://example.com/model.bin", destination.string(), 15, "abcd"}},
572+ nullptr, false, baseDir.string()), DownloadError::INVALID_ARGUMENT);
573+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
574+ "cleartext-source", {{"http://example.com/model.bin", destination.string(), 15, checksum}},
575+ nullptr, false, baseDir.string()), DownloadError::INVALID_ARGUMENT);
576+ EXPECT_EQ(ModelFileManager::Instance().DownloadModelFiles(
577+ "local-source", {{"file://" + source.string(), destination.string(), 15, checksum}},
578+ nullptr, false, baseDir.string()), DownloadError::INVALID_ARGUMENT);
579+ EXPECT_FALSE(std::filesystem::exists(destination));
580+ 
581+ std::filesystem::remove_all(baseDir);
582+}
583+ 
584+// Verifies secure download URL validation accepts arbitrary HTTPS hosts and blocks unsafe URL forms.
585+TEST_F(ModelDownloaderTest, SecureModelDownloadPolicyAcceptsCustomHttpsSources)
586+{
587+ const std::vector<std::string> secureUrls {
588+ "https://modelscope.cn/models/MNN/Qwen3-0.6B-MNN/resolve/master/llm.mnn",
589+ "https://custom.example/model.bin",
590+ "https://models.example.com:8443/path/model.bin?revision=v2&token=encoded%20value",
591+ "HTTPS://EXAMPLE.COM/model.bin",
592+ };
593+ for (const auto& url : secureUrls) {
594+ EXPECT_TRUE(IsSecureModelDownloadUrl(url)) << url;
595+ EXPECT_TRUE(IsAllowedModelDownloadSource(url)) << url;
596+ }
597+ 
598+ const std::string& originalUrl = secureUrls.front();
599+ EXPECT_TRUE(IsSecureModelDownloadRedirect(originalUrl, secureUrls[1]));
600+ EXPECT_TRUE(IsAllowedModelDownloadRedirect(originalUrl, secureUrls[1]));
601+ EXPECT_FALSE(IsSecureModelDownloadUrl("http://example.com/model.bin"));
602+ EXPECT_FALSE(IsSecureModelDownloadUrl("file:///etc/passwd"));
603+ EXPECT_FALSE(IsSecureModelDownloadUrl("https://user:password@example.com/model.bin"));
604+ EXPECT_FALSE(IsSecureModelDownloadUrl("https://example.com/model file.bin"));
605+ EXPECT_FALSE(IsSecureModelDownloadUrl("https:///missing-host.bin"));
606+ EXPECT_FALSE(IsSecureModelDownloadRedirect(originalUrl, "http://example.com/model.bin"));
607+ std::string curlUrl;
608+ EXPECT_FALSE(ResolveAllowedModelDownloadUrl("file:///tmp/model.bin", curlUrl));
609+ EXPECT_FALSE(ResolveAllowedModelDownloadUrl("file+test:///tmp/model.bin?token=secret", curlUrl));
610+ EXPECT_FALSE(ResolveAllowedModelDownloadUrl("file+test://host/model.bin", curlUrl));
611+#ifdef SMARTSERVE_TEST_FILE_URLS_ENABLED
612+ EXPECT_TRUE(ResolveAllowedModelDownloadUrl("file+test:///tmp/model.bin", curlUrl));
613+ EXPECT_EQ(curlUrl, "file:///tmp/model.bin");
614+ EXPECT_TRUE(IsAllowedModelDownloadRedirect("file+test:///tmp/model.bin", "file:///tmp/model.bin"));
615+#else
616+ EXPECT_FALSE(ResolveAllowedModelDownloadUrl("file+test:///tmp/model.bin", curlUrl));
617+#endif
618+}
619+ 
620+// Ensures download URL logging removes credentials, query parameters, and fragments.
621+TEST_F(ModelDownloaderTest, RedactUrlForLogRemovesSensitiveComponents)
622+{
623+ EXPECT_EQ(RedactUrlForLog("https://user:password@example.com:8443/models/model.bin?token=secret#fragment"),
624+ "https://example.com:8443/models/model.bin");
625+ EXPECT_EQ(RedactUrlForLog("https://example.com/model.bin?X-Amz-Signature=secret"),
626+ "https://example.com/model.bin");
627+ EXPECT_EQ(RedactUrlForLog("not a valid URL"), "<invalid-url>");
628+}
629+ 
459TEST_F(ModelDownloaderTest, ModelFileMetadataRoundTripsAndListsInstalledModels)630TEST_F(ModelDownloaderTest, ModelFileMetadataRoundTripsAndListsInstalledModels)
460{631{
461 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_metadata";632 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_metadata";
@@ -945,6 +1116,9 @@ TEST_F(ModelDownloaderTest, ModelFileManagerRejectsSymlinkAndFifoInstalledMetada
945 1116 
946TEST_F(ModelDownloaderTest, ModelFileManagerPauseResumeRequeuesRunningAndPending)1117TEST_F(ModelDownloaderTest, ModelFileManagerPauseResumeRequeuesRunningAndPending)
947{1118{
1119+#ifndef SMARTSERVE_TEST_FILE_URLS_ENABLED
1120+ GTEST_SKIP() << "requires -DSMARTSERVE_ENABLE_TEST_FILE_URLS=ON";
1121+#endif
948 const std::string modelId = "manager-pause-resume-test";1122 const std::string modelId = "manager-pause-resume-test";
949 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_pause_resume";1123 const auto baseDir = std::filesystem::temp_directory_path() / "smartserve_model_file_manager_pause_resume";
950 const auto sourceA = baseDir / "source_a.bin";1124 const auto sourceA = baseDir / "source_a.bin";
@@ -971,8 +1145,10 @@ TEST_F(ModelDownloaderTest, ModelFileManagerPauseResumeRequeuesRunningAndPending
971 1145 
972 ModelFileManager::Instance().SetMaxConcurrentDownloads(1);1146 ModelFileManager::Instance().SetMaxConcurrentDownloads(1);
973 std::vector<ModelFileManager::FileDownloadSpec> files = {1147 std::vector<ModelFileManager::FileDownloadSpec> files = {
974- {"file://" + sourceA.string(), destA.string(), static_cast<int64_t>(kFileSize), ""},1148+ {"file+test://" + sourceA.string(), destA.string(), static_cast<int64_t>(kFileSize),
975- {"file://" + sourceB.string(), destB.string(), static_cast<int64_t>(kFileSize), ""},1149+ ModelDownloader::CalculateSHA256(sourceA.string())},
1150+ {"file+test://" + sourceB.string(), destB.string(), static_cast<int64_t>(kFileSize),
1151+ ModelDownloader::CalculateSHA256(sourceB.string())},
976 };1152 };
977 1153 
978 std::atomic<bool> pauseAttempted{false};1154 std::atomic<bool> pauseAttempted{false};
@@ -1284,6 +1460,17 @@ TEST_F(ModelConfigTest, RejectsInvalidOrUnrepresentableFileSizes)
1284 })"));1460 })"));
1285}1461}
1286 1462 
1463+// Verifies model identifiers cannot inject path components into SDK-managed storage keys.
1464+TEST_F(ModelConfigTest, RejectsUnsafeModelIdentifiers)
1465+{
1466+ auto& configMgr = ModelConfigManager::Instance();
1467+ EXPECT_FALSE(configMgr.LoadFromString(R"({"models":[{"id":"../escape"}]})"));
1468+ EXPECT_FALSE(configMgr.LoadFromString(R"({"models":[{"id":"model/path"}]})"));
1469+ EXPECT_FALSE(configMgr.LoadFromString(R"({"models":[{"id":"model space"}]})"));
1470+ EXPECT_FALSE(configMgr.LoadFromString(R"({"models":[{"id":"."}]})"));
1471+ EXPECT_TRUE(configMgr.LoadFromString(R"({"models":[{"id":"model.v2_test-1"}]})"));
1472+}
1473+ 
1287// max_input_tokens 和 max_output_tokens 是可选字段:缺省时加载成功,对应配置项为空1474// max_input_tokens 和 max_output_tokens 是可选字段:缺省时加载成功,对应配置项为空
1288TEST_F(ModelConfigTest, TokenLimitsAreOptional)1475TEST_F(ModelConfigTest, TokenLimitsAreOptional)
1289{1476{
@@ -75,6 +75,14 @@ private:
75 bool validResConfig_;75 bool validResConfig_;
76};76};
77 77 
78+Model* AddCachedModel(ModelManager& manager, const std::string& name, bool validResConfig = true)
79+{
80+ auto model = std::make_unique<Model>(std::make_unique<FakeModelAdapter>(name, validResConfig));
81+ Model* rawModel = model.get();
82+ EXPECT_TRUE(manager.allModels_.insert({name, std::move(model)}).second);
83+ return rawModel;
84+}
85+ 
78TEST(ModelManagerTest, RegisterAndGetCreatorSuccess)86TEST(ModelManagerTest, RegisterAndGetCreatorSuccess)
79{87{
80 ModelManager manager;88 ModelManager manager;
@@ -93,9 +101,9 @@ TEST(ModelManagerTest, RegisterDuplicateCreatorFails)
93TEST(ModelManagerTest, GetOrCreateModelCachesInstance)101TEST(ModelManagerTest, GetOrCreateModelCachesInstance)
94{102{
95 ModelManager manager;103 ModelManager manager;
96- ASSERT_TRUE(manager.Register(std::make_unique<FakeModelCreator>("model-A", true)));104+ // A ready model must be returned from the cache without invoking a creator.
105+ Model* first = AddCachedModel(manager, "model-A");
97 106 
98- Model* first = manager.GetOrCreateModel("model-A");
99 Model* second = manager.GetOrCreateModel("model-A");107 Model* second = manager.GetOrCreateModel("model-A");
100 108 
101 ASSERT_NE(first, nullptr);109 ASSERT_NE(first, nullptr);
@@ -108,11 +116,12 @@ TEST(ModelManagerTest, GetOrCreateModelReturnsNullWhenCreatorNotRegistered)
108 ASSERT_EQ(manager.GetOrCreateModel("not-exist"), nullptr);116 ASSERT_EQ(manager.GetOrCreateModel("not-exist"), nullptr);
109}117}
110 118 
111-TEST(ModelManagerTest, GetOrCreateModelReturnsNullForInvalidAdapterResConfig)119+TEST(ModelManagerTest, GetOrCreateModelRejectsLegacyCreatorWithoutIntegrityManifest)
112{120{
113 ModelManager manager;121 ModelManager manager;
114- ASSERT_TRUE(manager.Register(std::make_unique<FakeModelCreator>("bad-model", false)));122+ // Legacy creators cannot provide a checksum manifest and must not load a model.
115- ASSERT_EQ(manager.GetOrCreateModel("bad-model"), nullptr);123+ ASSERT_TRUE(manager.Register(std::make_unique<FakeModelCreator>("legacy-model", true)));
124+ EXPECT_EQ(manager.GetOrCreateModel("legacy-model"), nullptr);
116}125}
117 126 
118TEST(ModelManagerTest, UnregisterRemovesCreator)127TEST(ModelManagerTest, UnregisterRemovesCreator)
@@ -132,8 +141,8 @@ TEST(ModelManagerTest, UnregisterMissingCreatorFails)
132TEST(ModelManagerTest, RemoveModelReturnsOwnedInstanceAndClearsCache)141TEST(ModelManagerTest, RemoveModelReturnsOwnedInstanceAndClearsCache)
133{142{
134 ModelManager manager;143 ModelManager manager;
135- ASSERT_TRUE(manager.Register(std::make_unique<FakeModelCreator>("model-A", true)));144+ // Removing a cached instance transfers ownership and makes later lookups miss.
136- Model* model = manager.GetOrCreateModel("model-A");145+ Model* model = AddCachedModel(manager, "model-A");
137 ASSERT_NE(model, nullptr);146 ASSERT_NE(model, nullptr);
138 147 
139 auto removed = manager.RemoveModel(model);148 auto removed = manager.RemoveModel(model);
@@ -64,10 +64,11 @@ public:
64 Release();64 Release();
65 }65 }
66 66 
67- static void Callback(const char*, int64_t downloaded, int64_t total, float, void* userData)67+ // Blocks on the first callback because file:// transfers can report only 0 or final progress.
68+ static void Callback(const char*, int64_t, int64_t, float, void* userData)
68 {69 {
69 auto* progress = static_cast<BlockingDownloadProgress*>(userData);70 auto* progress = static_cast<BlockingDownloadProgress*>(userData);
70- if (progress == nullptr || downloaded <= 0 || downloaded >= total) {71+ if (progress == nullptr) {
71 return;72 return;
72 }73 }
73 progress->gate_.EnterAndWait();74 progress->gate_.EnterAndWait();