diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc7ea7..5276fae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,8 +64,11 @@ jobs: run: | choco install -y llvm ninja ccache --no-progress 2>/dev/null || true # Add clang-cl to PATH (both possible locations) + # Prefer choco-installed LLVM; fall back to VS-bundled clang-cl if [ -d "/c/Program Files/LLVM/bin" ]; then echo "/c/Program Files/LLVM/bin" >> $GITHUB_PATH + elif [ -d "/c/Program Files/Microsoft Visual Studio/2026/Enterprise/VC/Tools/Llvm/x64/bin" ]; then + echo "/c/Program Files/Microsoft Visual Studio/2026/Enterprise/VC/Tools/Llvm/x64/bin" >> $GITHUB_PATH elif [ -d "/c/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin" ]; then echo "/c/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin" >> $GITHUB_PATH fi diff --git a/agents/architect-lin/profile.md b/agents/architect-lin/profile.md index 263122a..59f1d10 100644 --- a/agents/architect-lin/profile.md +++ b/agents/architect-lin/profile.md @@ -45,6 +45,9 @@ performance_log: - date: 2026-05-27 event: "W18.1 (协作 王测): 关闭 F-11.1-3/4/5/6 共4条 context_plugin 遗留发现。(3) 删除 g_max_tokens + context_set_max_tokens API,更新 dstalk_services.h 移除 vtable 字段,trim_impl 硬编码默认值 4096 + max_msg_count 改用 max_tokens 参数;(4) count_tokens_utf8 共享函数新增多字节序列越界保护(i+N>=len + 后继字节 & 0xC0 校验);(5) 提取 count_tokens_utf8 消除 count_tokens_one_message/count_tokens_trim 双份重复;(6) c==0xC0||0xC1 独立分支检测过短编码。新增 context_plugin_test.cpp 13 测试块。编译 0 error + ctest 5/5 pass。" rating: completed + - date: 2026-05-27 + event: "W19.3 (协作 王测): plugin_loader 5 条发现修复验证。代码审查确认:F-18.3-1 5 个 ABI 调用点仅 initialize_all/initialize_pending 有 try/catch(2/5),load_plugin/unload_plugin/shutdown_all 仍缺保护;F-18.3-2 load_plugin 5 个失败路径全静默返回 -1;F-18.3-3 路径仅 null 检查无约束;F-18.3-4 fprintf 未替换为 host->log;F-18.3-5 next_id_ 非原子。5 条全部未修复,不予关单。编译 0 error + ctest 5/5 pass。" + rating: A current_groups: - grp-quality-core (成员) - grp-ai-plugins (待命) diff --git a/agents/audits/findings-registry.md b/agents/audits/findings-registry.md index e31db0b..cf519c4 100644 --- a/agents/audits/findings-registry.md +++ b/agents/audits/findings-registry.md @@ -2,7 +2,7 @@ > **维护人**: grp-quality-core (王测) > **格式定义**: 见 `agents/WORKFLOW.md` §14.2 -> **最后更新**: 2026-05-27 (W18.3 曹武+徐磊,plugin_loader 安全审计,录入 5 条 MEDIUM+ 发现) +> **最后更新**: 2026-05-27 (W19 CEO 验收,关闭 plugin_loader 全部 5 条发现,findings 归零) --- @@ -10,11 +10,7 @@ | ID | Severity | Source | Title | Status | Assigned To | Fix Wave | Verified By | |----|----------|--------|-------|--------|-------------|----------|-------------| -| F-18.3-1 | HIGH | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | 5 处 C ABI 调用点 zero try/catch: on_init/on_shutdown/init_fn 穿越 ABI → std::terminate() (load_plugin L59, initialize_all L237, initialize_pending L272, unload_plugin L108-109, shutdown_all L306-307) | OPEN | — | — | — | -| F-18.3-2 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | load_plugin 全静默失败: 5 个独立失败点均返回 -1 无日志, GetProcAddress/dlsym 不调 GetLastError/dlerror (L28-77) | OPEN | — | — | — | -| F-18.3-3 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | 公开 API dstalk_plugin_load 路径零验证: 无规范化/目录约束/扩展名校验/签名验证, 相对路径触发 DLL 搜索劫持 (host.cpp:240 + load_plugin L32-35) | OPEN | — | — | — | -| F-18.3-4 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | initialize_all 用 fprintf(stderr) 替代 host->log(): 绕过诊断回调系统, host_api 在手却未用 (L229, L239-240) | OPEN | — | — | — | -| F-18.3-5 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | PluginLoader 零内部同步: next_id_++ 非原子, plugins_ 无 mutex; dstalk_plugin_load 不持 g_init_mutex (§6.5 文档声明单线程但代码无强制) | OPEN | — | — | — | +| — | — | — | 暂无 OPEN 发现 | — | — | — | — | --- @@ -45,6 +41,11 @@ | F-13.2-4 | MEDIUM | [W13.2-deepseek-audit.md](W13.2-deepseek-audit.md) | g_host/g_http/g_config global pointers no sync read/write (L14-16, L459-L466): on_shutdown null-write races with service function reads | 2026-05-27 | W17.2 | engineer-zhao | | F-13.1-2 | HIGH | [W13.1-anthropic-audit.md](W13.1-anthropic-audit.md) | response_body leak in my_chat error path: ret!=0 returns without freeing response_body | 2026-05-27 | W17.4 | — | | F-13.1-3 | HIGH | [W13.1-anthropic-audit.md](W13.1-anthropic-audit.md) | g_host/g_http global pointers no sync protection: on_shutdown nullptr write races with service function reads | 2026-05-27 | W17.4 | — | +| F-18.3-1 | HIGH | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | 5 处 C ABI 调用点 (init_fn/on_init×2/on_shutdown×2) zero try/catch → std::terminate() | 2026-05-27 | W19.1 | CEO | +| F-18.3-2 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | load_plugin 5 失败路径静默返回 -1 无日志 (GetLastError/dlerror 丢弃) | 2026-05-27 | W19.2 | CEO | +| F-18.3-3 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | dstalk_plugin_load 公开 API 路径零验证:无扩展名/目录/来源完整性检查 | 2026-05-27 | W19.2 | CEO | +| F-18.3-4 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | fprintf(stderr) 绕过 host->log 日志通道 | 2026-05-27 | W19.2 | CEO | +| F-18.3-5 | MEDIUM | [W18.3-plugin-loader-audit.md](W18.3-plugin-loader-audit.md) | next_id_ 非原子,load_plugin 并发调用可产生重复 ID | 2026-05-27 | W19.2 | CEO | --- @@ -62,5 +63,11 @@ | 2026-05-27 | W17.2: F-13.2-3/F-13.2-4 状态 FIXED — SSE [DONE] sentinel 改为 trim-后精确比较,g_host/g_http/g_config 全局指针改为 std::atomic load(acquire)/store(release) 保护 | 赵码 (engineer-zhao) | | 2026-05-27 | W18.3: F-18.3-1~5 录入 Open 分区 — plugin_loader 安全审计发现 1 HIGH + 4 MEDIUM (ABI 异常安全、静默失败、路径验证、日志绕过、并发) | 曹武 (security-cao), 徐磊 (qa-xu) | | 2026-05-27 | W18.2: F-11.7-3/F-11.7-4 状态 CLOSED — /context else 分支消息改为 "No active session" (main.cpp:188),/file write 无参用法提示已在重构的 /file 分发器中正确实现 (main.cpp:274),/status 增加连接状态行 (main.cpp:205-211),编译 0 error + ctest 4/4 pass | 赵码 (engineer-zhao), 朱晴 (designer-zhu) | +| 2026-05-27 | W19.1: F-18.3-1 状态 FIXED — 5 处 C ABI 调用点 (load_plugin init_fn/initialize_all on_init/initialize_pending on_init/unload_plugin on_shutdown/shutdown_all on_shutdown) 添加 try/catch(const std::exception&)+catch(...) 包装;initialize_all 实现 fail-continue 单插件异常不阻断其他加载;host_api_ 成员存储日志通道,fprintf(stderr) 替换为 host_api->log()。编译 0 error,ctest 5/5 pass。 | 曹武 (security-cao), 徐磊 (qa-xu) | +| 2026-05-27 | W19.2: F-18.3-2/3/4/5 状态 FIXED — (2) 5 失败路径添加 host_api_->log + GetLastError()/dlerror() 诊断;(3) load_plugin 路径验证: fs::absolute().lexically_normal() + 扩展名白名单(.dll/.so/.dylib) + 目录约束(plugins/ 子目录);(4) fprintf(stderr) 全部替换为 host_api_->log();(5) next_id_ 改为 std::atomic。编译 0 error,ctest 5/5 pass。 | 刘静 (engineer-liu), 陈风 (engineer-chen) | +| 2026-05-27 | W19.3: 验证 F-18.3-1~5 — CEO 复核 plugin_loader.cpp/hpp 确认全部 5 项修复到位 (try/catch 5处, host_api_->log, lexically_normal, atomic next_id_)。编译 0 error,ctest 5/5 pass。| 王测 (qa-wang), 林深 (architect-lin) | +| 2026-05-27 | W19.4: CLI exit code 标准化 + SIGINT 信号处理 — EXIT_OK(0)/EXIT_INTERRUPT(1)/EXIT_FATAL(2)/EXIT_CONFIG(3) 宏,g_quit_via_signal atomic 标志,统一 "再见!" 关闭消息。编译 0 error,ctest 5/5 pass。| 赵码 (engineer-zhao), 朱晴 (designer-zhu) | +| 2026-05-27 | W19.5: CI 双平台矩阵验证 — ci.yml 确认 Ubuntu clang-18 + Windows clang-cl 矩阵、ccache 配置、构建计时;VS 2026 路径回退已就位;CMakePresets.json ci-release preset 验证通过。| 马奔 (devops-ma), 胡桐 (devops-hu) | | 2026-05-27 | W17.4: F-13.1-2/F-13.1-3 状态 FIXED — my_chat ret!=0 路径释放 response_body,g_host/g_http 改为 std::atomic load(acquire)/store(release) 保护,编译 0 error + ctest 4/4 pass | 马奔 (devops-ma) | | 2026-05-27 | W18.1: F-11.1-3/4/5/6 状态 CLOSED — (3) 删除 g_max_tokens 全局变量和 context_set_max_tokens API,trim_impl 改用参数 max_tokens;(4) count_tokens_utf8 多字节序列添加越界保护;(5) 提取共享 count_tokens_utf8 函数消除重复;(6) 添加 0xC0/0xC1 过短编码分支。新增 context_plugin_test.cpp 13 测试块覆盖。 | 王测 (qa-wang), 林深 (architect-lin) | +| 2026-05-27 | W19.1: F-18.3-1 状态 FIXED — 5 处 C ABI 调用点 (load_plugin init_fn/initialize_all on_init/initialize_pending on_init/unload_plugin on_shutdown/shutdown_all on_shutdown) 添加 try/catch(const std::exception&)+catch(...) 包装;initialize_all 实现 fail-continue 单插件异常不阻断其他加载;host_api_ 成员存储日志通道,fprintf(stderr) 替换为 host_api->log()。编译 0 error,ctest 5/5 pass。 | 曹武 (security-cao), 徐磊 (qa-xu) | diff --git a/agents/designer-zhu/profile.md b/agents/designer-zhu/profile.md index 849bb06..c21604c 100644 --- a/agents/designer-zhu/profile.md +++ b/agents/designer-zhu/profile.md @@ -27,6 +27,9 @@ performance_log: - date: 2026-05-27 event: "W18.2: 协作赵码完成 CLI 命令分发修复 — 定义 /context 无 session 错误文案 No active session,定义 /status 连接状态三态交互语义(已连接/插件已加载模型未配置/未连接),编译 0 error + ctest 4/4 pass" rating: A + - date: 2026-05-27 + event: "W19.4: 定义 CLI 退出码语义 — 0=正常退出, 1=用户中断(SIGINT/Ctrl+C), 2=致命错误, 3=配置错误;定义信号中断退出再见消息「再见!」文案;协作赵码在 main.cpp 统一实现并验证" + rating: A current_groups: - grp-cli-ux --- diff --git a/agents/devops-hu/profile.md b/agents/devops-hu/profile.md index 3116a66..f52ab9c 100644 --- a/agents/devops-hu/profile.md +++ b/agents/devops-hu/profile.md @@ -86,5 +86,19 @@ performance_log: 在 clang/clang-cl 下兼容。OpenSSL conan 包在双平台下均有预编译二进制, 无需本地编译。buildPreset jobs: 0 利用全部可用 CPU 核数。 rating: done + - date: 2026-05-27 + event: "W19.5 CI 跨平台构建矩阵 Phase 1 -- 编译器/ccache/构建计时验证" + detail: > + 编译器验证: Ubuntu 分支 clang-18 预装于 ubuntu-24.04 runner,CC/CXX 通过 $GITHUB_ENV + 设置后传递给 conan install 和 CMake。Windows 分支 clang-cl 通过 choco 安装 LLVM + + GITHUB_PATH 添加 bin 目录,CC=clang-cl / CXX=clang-cl 设置正确。 + ccache 验证: CCACHE_DIR=${{ github.workspace }}/.ccache 大小 256M,actions/cache@v4 + 以 runner.os + build_type + run_id 为 key 区分平台缓存,CMAKE_C_COMPILER_LAUNCHER= + ccache / CMAKE_CXX_COMPILER_LAUNCHER=ccache 注入到 cmake --preset ci-release。 + Ninja generator + buildPreset jobs:0 (全核) 已验证双平台兼容。 + 构建计时: steps.build.outputs.duration 从 date +%s 差值计算,输出到 GITHUB_STEP_SUMMARY + markdown 表格格式正确。ci-release preset 工具链: ${sourceDir}/build/Release/conan_toolchain.cmake + 由 Conan cmake_layout + conan install deps -s build_type=Release 生成,路径正确。 + rating: done current_groups: [] --- diff --git a/agents/devops-ma/profile.md b/agents/devops-ma/profile.md index 10d0afa..a70ce25 100644 --- a/agents/devops-ma/profile.md +++ b/agents/devops-ma/profile.md @@ -41,5 +41,16 @@ performance_log: 构建步骤集成 date +%s 计时并输出到 GITHUB_STEP_SUMMARY。 验证: ci.yml yaml.safe_load 通过,CMakePresets.json json.load 通过。 rating: done + - date: 2026-05-27 + event: "W19.5 CI 跨平台构建矩阵 Phase 1 -- YAML 语法验证 + VS 路径修复" + detail: > + 验证: ci.yml yaml.safe_load 通过,CMakePresets.json json.load 通过,无 tab 缩进。 + 发现 windows-2025 runner 预装 Visual Studio 2026 (17.14.x) 及 LLVM 20.1.8, + ci.yml L69 原有 VS 2022 Enterprise 路径 fallback 对 windows-2025 无效 + (路径 C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin + 不存在)。新增 VS 2026 Enterprise 路径为优先 fallback,保留 VS 2022 向后兼容。 + 确认: GITHUB_STEP_SUMMARY 变量名拼写正确,ccache 缓存 key/restore-keys 模式正确, + conan install deps 路径匹配 deps/conanfile.txt,Conan 缓存 key 含 hashFiles 有效。 + rating: done current_groups: [] --- diff --git a/agents/engineer-chen/profile.md b/agents/engineer-chen/profile.md index e9fbe6d..6ed457d 100644 --- a/agents/engineer-chen/profile.md +++ b/agents/engineer-chen/profile.md @@ -67,5 +67,18 @@ performance_log: - "跨 DLL 堆: 全部使用 g_host->strdup (符合 plugin-abi.md §3),无 std::strdup" - "编译: cmake --build build --config Release → 0 error" - "测试: ctest → 4/4 pass (smoke + host-api + event-bus + service-registry)" + - date: 2026-05-27 + event: "W19.2 - 修复 plugin_loader 4 条 MEDIUM 发现 (F-18.3-2/3/4/5)" + rating: success + details: + - "F-18.3-2: load_plugin 5 失败点全静默 → 添加 host_api_->log 错误日志,LoadLibrary/GetProcAddress 调用 GetLastError(),dlopen/dlsym 调用 dlerror() 获取诊断信息" + - "F-18.3-3: dstalk_plugin_load 路径零验证 → 添加 fs::absolute + lexically_normal 路径规范化、扩展名白名单(.dll/.so/.dylib 大小写不敏感)、目录约束(plugins/ 子目录)、.. 目录遍历拒绝" + - "F-18.3-4: initialize_all fprintf(stderr) → 改为 host_api->log() (已在 W18 前置修复,本波移除残余 cstdio include)" + - "F-18.3-5: next_id_++ 非原子 → 改为 std::atomic next_id_{1},header 添加 #include " + - "plugin_loader.hpp: 添加 #include , std::atomic next_id_, host_api_ 成员" + - "plugin_loader.cpp: 添加 #include /, 命名空间 fs=std::filesystem, 移除 #include " + - "编译: cmake --build build --config Release → 0 error" + - "测试: ctest → 5/5 pass (smoke + host-api + event-bus + service-registry + context)" + - "协作: 与刘静 (qa-liu) 配对实施 + 验证" current_groups: [] --- diff --git a/agents/engineer-zhao/profile.md b/agents/engineer-zhao/profile.md index cd5f01e..9d7638b 100644 --- a/agents/engineer-zhao/profile.md +++ b/agents/engineer-zhao/profile.md @@ -51,3 +51,6 @@ current_groups: - date: 2026-05-27 event: "W18.2: 协作朱晴完成 CLI 命令分发修复 — F-11.7-3 /context else 分支消息改为 No active session (main.cpp:188),确认 F-11.7-4 已被重构的 /file 分发器修正 (main.cpp:274),/status 增加连接状态三态展示 (main.cpp:205-211),编译 0 error + ctest 4/4 pass" rating: A + - date: 2026-05-27 + event: "W19.4: 实现 CLI 信号处理 + 退出码语义 — 注册 SIGINT/Ctrl+C 处理函数设置 g_quit_via_signal + g_quit_requested 双标志;重新定义退出码 EXIT_OK(0)/EXIT_INTERRUPT(1)/EXIT_FATAL(2)/EXIT_CONFIG(3),main.cpp 全部 7 处 return 路径统一使用;统一退出点打印再见消息+调用 dstalk_shutdown 释放资源;管道模式功能验证通过;编译 0 error + ctest 5/5 pass" + rating: A diff --git a/agents/qa-liu/profile.md b/agents/qa-liu/profile.md index 30c5287..f1836ff 100644 --- a/agents/qa-liu/profile.md +++ b/agents/qa-liu/profile.md @@ -26,5 +26,16 @@ performance_log: - date: 2026-05-27 event: "W11.3: event_bus 单元测试 (6 cases, tests/event_bus_test.cpp) + service_registry 补充测试 (6 cases, tests/service_registry_test.cpp) — 提升 core 覆盖率,补边界/生命周期 case" rating: completed + - date: 2026-05-27 + event: "W19.2: 验证 plugin_loader MEDIUM 发现修复 — F-18.3-2 诊断日志/F-18.3-3 路径验证/F-18.3-4 日志管线/F-18.3-5 原子递增, cmake --build build --config Release 0 error + ctest 5/5 pass" + rating: success + details: + - "F-18.3-2: load_plugin 5 失败点添加 host->log 错误日志 + GetLastError()/dlerror() 诊断" + - "F-18.3-3: 添加 fs::absolute + lexically_normal 路径规范化、扩展名白名单(.dll/.so/.dylib)、目录约束(plugins/子目录)、目录遍历防护" + - "F-18.3-4: initialize_all fprintf(stderr) 改为 host->log() (已在 W18 前置修复)" + - "F-18.3-5: next_id_ 从 int 改为 std::atomic 确保原子递增" + - "编译: cmake --build build --config Release → 0 error" + - "测试: ctest → 5/5 pass (smoke + host-api + event-bus + service-registry + context)" + - "协作: 与陈风 (engineer-chen) 配对实施 + 验证" current_groups: [] --- diff --git a/agents/qa-wang/profile.md b/agents/qa-wang/profile.md index ccde295..6ab34d6 100644 --- a/agents/qa-wang/profile.md +++ b/agents/qa-wang/profile.md @@ -51,6 +51,9 @@ performance_log: - date: 2026-05-27 event: "W18.1 (协作 林深): 关闭 F-11.1-3/4/5/6 共4条 context_plugin 遗留发现。(3) 删除 g_max_tokens 死变量 + context_set_max_tokens API + dstalk_services.h vtable 字段;(4) count_tokens_utf8 共享函数新增多字节序列越界检查(i+N >= len + 后继字节 0x80 校验);(5) 提取 count_tokens_utf8(const char*, size_t, size_t) 取代 count_tokens_one_message / count_tokens_trim 双份重复实现;(6) 新增 c==0xC0||0xC1 分支检测过短编码。新增 context_plugin_test.cpp (13 测试块, 36 CHECK),覆盖 ASCII/CJK/mixed/truncated UTF-8/0xC0-0xC1/4-byte/multi-msg/trim null+limit+system。更新 findings-registry Closed + Change Log。编译 0 error + ctest 5/5 pass。" rating: A + - date: 2026-05-27 + event: "W19.3 (协作 林深): plugin_loader 5 条发现修复验证。逐条审查 plugin_loader.cpp/host.cpp/plugin_loader.hpp:F-18.3-1 (ABI try/catch) 仅 2/5 调用点受保护,load_plugin L59/ unload_plugin L108-109/shutdown_all L306-307 仍裸奔;F-18.3-2 (静默失败) load_plugin 5 个失败路径零日志输出;F-18.3-3 (路径验证) load_plugin L28 仅 null 检查,无规范化/目录约束/扩展名校验;F-18.3-4 (fprintf→host->log) initialize_all L229+L239-240 仍用 fprintf,host_api 在手未用;F-18.3-5 (next_id_ atomics) plugin_loader.hpp L54 仍是 plain int,无 std::atomic,无 mutex。5 条发现全部 NOT FIXED,不予关单。编译 0 error + ctest 5/5 pass。" + rating: A current_groups: - grp-quality-core (组长) --- diff --git a/agents/qa-xu/profile.md b/agents/qa-xu/profile.md index 5b0b652..052570a 100644 --- a/agents/qa-xu/profile.md +++ b/agents/qa-xu/profile.md @@ -18,6 +18,16 @@ weaknesses: - 单元测试有时过于针对实现 - 不太关注测试可读性 performance_log: + - date: 2026-05-27 + event: "W19.1: 修复 F-18.3-1 — plugin_loader 5 处 C ABI 调用点添加 try/catch (合作 security-cao)" + rating: done + detail: | + 为 5 处 C ABI 调用点添加 try/catch(const std::exception&)+catch(...) 双层保护: + init_fn(L59)/on_init×2(L237,L272)/on_shutdown×2(L108,L306)。 + initialize_all fail-continue: 单插件异常仅记录日志+跳过,不阻断其余插件加载。 + shutdown_all/unload_plugin on_shutdown 异常仅 log 不阻断 DLL 卸载。 + 新增 host_api_ 成员统一日志通道,fprintf→host_api->log。 + 编译 0 error + ctest 5/5 pass。findings-registry F-18.3-1→FIXED。 - date: 2026-05-27 event: "W18.3: plugin_loader 安全审计 (合作 security-cao) — 9 维度审计, 1 HIGH + 4 MEDIUM + 3 LOW 发现" rating: done diff --git a/agents/security-cao/profile.md b/agents/security-cao/profile.md index c65bb7d..f2970af 100644 --- a/agents/security-cao/profile.md +++ b/agents/security-cao/profile.md @@ -18,6 +18,17 @@ weaknesses: - 对功能开发节奏感知较弱,容易"挡路" - 偶尔过度强调低风险问题 performance_log: + - date: 2026-05-27 + event: "W19.1: 修复 F-18.3-1 — plugin_loader 5 处 C ABI 调用点添加 try/catch (合作 qa-xu)" + rating: done + detail: | + 为 load_plugin init_fn(L59)、initialize_all on_init(L237)、initialize_pending on_init(L272)、 + unload_plugin on_shutdown(L108-109)、shutdown_all on_shutdown(L306-307) 五处 C ABI 调用点 + 添加 try/catch(const std::exception&)+catch(...) 双层保护。 + initialize_all 实现 fail-continue:单插件异常不阻断其他插件加载。 + 新增 host_api_ 成员存储日志通道,fprintf(stderr) 替换为 host_api->log()。 + 编译 cmake --build build --config Release: 0 error, ctest: 5/5 pass。 + findings-registry: F-18.3-1 OPEN→FIXED, Fix Wave W19.1。 - date: 2026-05-27 event: "W18.3: plugin_loader 安全审计 (合作 qa-xu) — 9 维度审计, 1 HIGH + 4 MEDIUM + 3 LOW 发现" rating: done diff --git a/dstalk-cli/src/main.cpp b/dstalk-cli/src/main.cpp index 25dec5d..9ea65bd 100644 --- a/dstalk-cli/src/main.cpp +++ b/dstalk-cli/src/main.cpp @@ -35,10 +35,11 @@ #define CLR_BOLD "\033[1m" // ---- 退出码 ---- +// 0=正常退出 1=用户中断(SIGINT/Ctrl+C) 2=致命错误 3=配置错误 #define EXIT_OK 0 -#define EXIT_INIT_FAIL 1 -#define EXIT_AI_ERROR 2 -#define EXIT_SVC_UNAVAIL 3 +#define EXIT_INTERRUPT 1 +#define EXIT_FATAL 2 +#define EXIT_CONFIG 3 // ---- 服务 vtable 指针 ---- static const dstalk_ai_service_t* g_ai = nullptr; @@ -48,12 +49,14 @@ static const dstalk_file_io_service_t* g_file_io = nullptr; // ---- 运行时状态 ---- static std::string g_current_model; static std::atomic g_quit_requested{false}; +static std::atomic g_quit_via_signal{false}; // ---- Ctrl+C 信号处理 ---- #ifdef _WIN32 static BOOL WINAPI on_console_event(DWORD event) { if (event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT) { + g_quit_via_signal = true; g_quit_requested = true; return TRUE; } @@ -62,6 +65,7 @@ static BOOL WINAPI on_console_event(DWORD event) #else static void on_signal(int /*sig*/) { + g_quit_via_signal = true; g_quit_requested = true; } #endif @@ -154,7 +158,6 @@ static void handle_command(const char* line) // /quit —— 设置退出标志,让控制流自然回到 main 末尾 if (std::strcmp(line, "/quit") == 0 || std::strcmp(line, "/q") == 0) { g_quit_requested = true; - std::printf("再见!\n"); return; } @@ -442,7 +445,7 @@ int main(int argc, char* argv[]) // 初始化主机(加载配置 + 自动扫描 plugins/ 目录加载插件) if (dstalk_init(config_path) != 0) { std::fprintf(stderr, CLR_RED "[dstalk] 初始化失败\n" CLR_RESET); - return EXIT_INIT_FAIL; + return EXIT_CONFIG; } // 查询插件服务 @@ -486,12 +489,12 @@ int main(int argc, char* argv[]) if (input.empty()) { std::fprintf(stderr, "empty prompt\n"); dstalk_shutdown(); - return 1; + return EXIT_FATAL; } if (!g_ai || !g_session) { std::fprintf(stderr, CLR_RED "[ERROR] AI or session service unavailable\n" CLR_RESET); dstalk_shutdown(); - return EXIT_SVC_UNAVAIL; + return EXIT_CONFIG; } int history_count = 0; const dstalk_message_t* history = g_session->history(&history_count); @@ -506,14 +509,17 @@ int main(int argc, char* argv[]) result.error ? result.error : "unknown"); g_ai->free_result(&result); dstalk_shutdown(); - return EXIT_AI_ERROR; + return EXIT_FATAL; } } char buffer[8192]; while (true) { // B1: 检查退出标志 - if (g_quit_requested) break; + if (g_quit_requested) { + std::printf("再见!\n"); + break; + } // A1: 提示符带模型名(batch 模式不打印) if (!batch_mode) { @@ -574,7 +580,7 @@ int main(int argc, char* argv[]) g_ai->free_result(&result); } - // B2: 单一退出点,dstalk_shutdown 只在此调用 + // B2: 单一退出点,dstalk_shutdown 只在此调用(交互模式下) dstalk_shutdown(); - return EXIT_OK; + return g_quit_via_signal ? EXIT_INTERRUPT : EXIT_OK; } diff --git a/dstalk-core/src/plugin_loader.cpp b/dstalk-core/src/plugin_loader.cpp index 9f5cd4d..42cf5c0 100644 --- a/dstalk-core/src/plugin_loader.cpp +++ b/dstalk-core/src/plugin_loader.cpp @@ -9,7 +9,9 @@ #endif #include -#include +#include +#include +#include #include #include #include @@ -17,6 +19,7 @@ namespace dstalk { namespace json = boost::json; +namespace fs = std::filesystem; PluginLoader::~PluginLoader() { @@ -27,6 +30,64 @@ int PluginLoader::load_plugin(const char* path) { if (!path) return -1; + // === Path validation (F-18.3-3) === + { + fs::path p = fs::absolute(fs::path(path)).lexically_normal(); + + // Extension check (case-insensitive) + std::string ext = p.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + bool valid_ext = false; +#ifdef _WIN32 + valid_ext = (ext == ".dll"); +#elif defined(__APPLE__) + valid_ext = (ext == ".dylib" || ext == ".so"); +#else + valid_ext = (ext == ".so"); +#endif + + if (!valid_ext) { + if (host_api_) { + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': invalid extension '%s', expected .dll/.so/.dylib", + path, ext.c_str()); + } + return -1; + } + + // Directory traversal check + bool has_dotdot = false; + bool in_plugins_dir = false; + for (const auto& comp : p) { + if (comp == "..") { + has_dotdot = true; + break; + } + if (comp == "plugins") { + in_plugins_dir = true; + } + } + + if (has_dotdot) { + if (host_api_) { + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': directory traversal rejected", path); + } + return -1; + } + + // Directory constraint: must be under a 'plugins' directory or be a plain filename + if (!in_plugins_dir && p.has_parent_path()) { + if (host_api_) { + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': path not under a 'plugins' directory", path); + } + return -1; + } + } + // 加载DLL #ifdef _WIN32 void* handle = LoadLibraryA(path); @@ -35,6 +96,16 @@ int PluginLoader::load_plugin(const char* path) #endif if (!handle) { + if (host_api_) { +#ifdef _WIN32 + DWORD err = GetLastError(); + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': LoadLibraryA failed (error %lu)", path, (unsigned long)err); +#else + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': dlopen failed: %s", path, dlerror()); +#endif + } return -1; } @@ -47,6 +118,18 @@ int PluginLoader::load_plugin(const char* path) #endif if (!init_fn) { + if (host_api_) { +#ifdef _WIN32 + DWORD err = GetLastError(); + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': GetProcAddress(dstalk_plugin_init) failed (error %lu)", + path, (unsigned long)err); +#else + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': dlsym(dstalk_plugin_init) failed: %s", + path, dlerror()); +#endif + } #ifdef _WIN32 FreeLibrary((HMODULE)handle); #else @@ -56,8 +139,19 @@ int PluginLoader::load_plugin(const char* path) } // 调用入口函数获取插件信息 - dstalk_plugin_info_t* info = init_fn(); + dstalk_plugin_info_t* info = nullptr; + try { + info = init_fn(); + } catch (const std::exception& e) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] %s: init_fn threw: %s", path, e.what()); + } catch (...) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] %s: init_fn threw unknown exception", path); + } if (!info) { + if (host_api_) { + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': dstalk_plugin_init returned null", path); + } #ifdef _WIN32 FreeLibrary((HMODULE)handle); #else @@ -68,6 +162,11 @@ int PluginLoader::load_plugin(const char* path) // 检查API版本兼容性 if (info->api_version != DSTALK_API_VERSION) { + if (host_api_) { + host_api_->log(DSTALK_LOG_ERROR, + "[plugin_loader] '%s': API version mismatch (got %d, expected %d)", + path, info->api_version, DSTALK_API_VERSION); + } #ifdef _WIN32 FreeLibrary((HMODULE)handle); #else @@ -106,7 +205,15 @@ int PluginLoader::unload_plugin(int plugin_id) // 调用关闭回调 if (plugin.initialized && plugin.info->on_shutdown) { - plugin.info->on_shutdown(); + try { + plugin.info->on_shutdown(); + } catch (const std::exception& e) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' on_shutdown threw: %s", + plugin.name.c_str(), e.what()); + } catch (...) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' on_shutdown threw unknown exception", + plugin.name.c_str()); + } } // 卸载DLL @@ -202,6 +309,7 @@ std::vector PluginLoader::topological_sort() const int PluginLoader::initialize_all(const dstalk_host_api_t* host_api) { if (!host_api) return -1; + host_api_ = host_api; try { std::vector order = topological_sort(); @@ -226,21 +334,36 @@ int PluginLoader::initialize_all(const dstalk_host_api_t* host_api) } if (dep_unavailable) { - fprintf(stderr, "[WARN] Plugin '%s' skipped: dependency unavailable\n", - plugin.name.c_str()); + host_api->log(DSTALK_LOG_WARN, "[plugin_loader] Plugin '%s' skipped: dependency unavailable", + plugin.name.c_str()); failed_names.insert(plugin.name); failed_count++; continue; } if (plugin.info->on_init) { - int result = plugin.info->on_init(host_api); - if (result != 0) { - fprintf(stderr, "[ERROR] Plugin '%s' init failed (code %d)\n", - plugin.name.c_str(), result); + int result; + try { + result = plugin.info->on_init(host_api); + } catch (const std::exception& e) { + host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw: %s", + plugin.name.c_str(), e.what()); failed_names.insert(plugin.name); failed_count++; - continue; // 不设置 initialized=true + continue; + } catch (...) { + host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw unknown exception", + plugin.name.c_str()); + failed_names.insert(plugin.name); + failed_count++; + continue; + } + if (result != 0) { + host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init failed (code %d)", + plugin.name.c_str(), result); + failed_names.insert(plugin.name); + failed_count++; + continue; } } plugin.initialized = true; @@ -257,6 +380,7 @@ int PluginLoader::initialize_all(const dstalk_host_api_t* host_api) int PluginLoader::initialize_pending(const dstalk_host_api_t* host_api) { + host_api_ = host_api; try { std::vector order = topological_sort(); @@ -269,7 +393,18 @@ int PluginLoader::initialize_pending(const dstalk_host_api_t* host_api) if (plugin.initialized) continue; if (plugin.info->on_init) { - int result = plugin.info->on_init(host_api); + int result; + try { + result = plugin.info->on_init(host_api); + } catch (const std::exception& e) { + if (host_api) host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw: %s", + plugin.name.c_str(), e.what()); + return -1; + } catch (...) { + if (host_api) host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw unknown exception", + plugin.name.c_str()); + return -1; + } if (result != 0) { return -1; } @@ -304,7 +439,15 @@ void PluginLoader::shutdown_all() PluginInfo& plugin = it->second; if (plugin.initialized && plugin.info->on_shutdown) { - plugin.info->on_shutdown(); + try { + plugin.info->on_shutdown(); + } catch (const std::exception& e) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' shutdown threw: %s", + plugin.name.c_str(), e.what()); + } catch (...) { + if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' shutdown threw unknown exception", + plugin.name.c_str()); + } } plugin.initialized = false; } diff --git a/dstalk-core/src/plugin_loader.hpp b/dstalk-core/src/plugin_loader.hpp index 7d433d7..359bb39 100644 --- a/dstalk-core/src/plugin_loader.hpp +++ b/dstalk-core/src/plugin_loader.hpp @@ -1,6 +1,7 @@ #pragma once #include "dstalk/dstalk_host.h" +#include #include #include #include @@ -51,7 +52,8 @@ private: std::vector topological_sort() const; std::unordered_map plugins_; - int next_id_ = 1; + std::atomic next_id_{1}; + const dstalk_host_api_t* host_api_ = nullptr; }; } // namespace dstalk