- W16.1 (曹武): F-11.7-1 CLOSED — confirmed W12.4 fix, corrupt binary eliminated - W16.2 (孙宇): F-11.1-1 FIXED — context_plugin.cpp try/catch on set_max_tokens + on_shutdown - W16.3 (陈风): F-11.1-2 CLOSED — confirmed W12.1 fix, strdup OOM protection already in place - W16.4 (胡桐): Integrate check_agents_metadata into refresh_status.py as pre-gate (error→exit 1) - W16.5 (周岩): Add Findings Summary to W13.3 network audit, register 3 findings - W16.6 (赵码): Add Findings Summary to W13.1+W13.2 AI audits, register 8 findings (4 already W14-fixed) Build 0 error, ctest 4/4 pass, metadata check 0 error 0 warning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 KiB
W13.2 DeepSeek Plugin Audit
Auditor: 孙宇 (engineer-sun) Date: 2026-05-27 File: plugins/deepseek/src/deepseek_plugin.cpp (486 行) Reference: plugin-abi.md §8 §9; anthropic_plugin.cpp (对比); W9.3 security-logging.md
文件总览
| 属性 | 值 |
|---|---|
| 总行数 | 486 |
| 函数数 | 15 (含 2 个生命周期 + 1 个导出入口) |
| C ABI 暴露入口 | 7 (on_init, on_shutdown, configure, chat, chat_stream, free_result, dstalk_plugin_init) |
| 依赖 | dstalk_host.h, dstalk_services.h, boost::json |
| 注册服务名 | "ai.deepseek" v1 |
维度 1: §5 + §8 异常安全 (C ABI 全部入口)
违反入口清单
所有通过 C ABI 暴露的函数均未包裹 try/catch:
| 函数 | 行号 | 可抛 C++ 操作 | 触发条件 |
|---|---|---|---|
my_configure |
238-256 | std::string::operator=, g_host->log |
bad_alloc, log 内部异常 |
my_chat |
261-301 | std::string, extract_host_port, build_request_json → json::parse(tools_json), append_history → json::parse(tool_calls_json) |
bad_alloc, 恶意/畸形 tools_json, 畸形 tool_calls_json |
my_chat_stream |
337-417 | 同上 | 同上 |
on_init |
443-457 | query_service, register_service, log |
bad_alloc (低概率) |
on_shutdown |
459-467 | secure_zero, std::string::clear |
实际不会抛异常 |
关键发现 1 — [严重] C++ 异常可穿越 ABI 边界导致 std::terminate()
- 行号: L91 (
append_history), L129 (build_request_json), L261-301 (my_chat), L337-417 (my_chat_stream) - 触发路径:
my_chat(history, ..., tools_json)→build_request_json→ L129json::parse(tools_json)— 若 tools_json 格式错误,json::parse抛出std::system_error。该异常在无 try/catch 的my_chat中穿越 C ABI 边界 →std::terminate()→ 进程崩溃。 - 同样路径: L91
json::parse(m.tool_calls_json)在append_history中 — 若历史消息包含畸形 tool_calls_json,同样触发。 - 对比 anthropic: anthropic 插件完全相同的漏洞 (W13.1 杨帆审计中),属结构性缺陷。
- 违反: plugin-abi.md §8.2 "每个使用 C++ STL 类型的 ABI 函数外层必须包裹 try/catch"。
发现 2 — [中] parse_response 有 try/catch 但 build_request_json 无
parse_response(L166-202) 内部有try/catch(std::exception&)/catch(...),自身安全。- 但其调用方
my_chat(L261) 在调用build_request_json(L278) 时无保护,而build_request_json在 L129json::parse(tools_json)可抛异常。 - 不对称保护: 响应解析安全,请求构建不安全。
维度 2: §9 字符串返回值生命周期
评级: A — 无违规。
| 检查点 | 行号 | 方法 | 判定 |
|---|---|---|---|
dstalk_chat_result_t.content |
L174, L413 | g_host->strdup(content.c_str()) |
模式 A (拥有权转移) |
dstalk_chat_result_t.error |
L151, L155, L158, L187, L194 等 | g_host->strdup(...) |
模式 A |
dstalk_chat_result_t.tool_calls_json |
L178 | g_host->strdup(tc.c_str()) |
模式 A |
my_free_result |
L422-428 | g_host->free() 逐个字段 |
正确释放 |
dstalk_plugin_info_t name/version/description |
L472-476 | 静态字符串字面量 | 模式 B (静态生命周期) |
- L151 临时
std::string的.c_str()传给strdup: 因临时对象在完整表达式结束时析构,strdup 在此之前完成复制,安全。✅ - L291
r.error = g_host->strdup("http request failed")— 字面量 → 堆副本,正确。✅
维度 3: §2 跨 DLL 堆纪律
评级: A — 零违规。
| 搜索项 | 结果 |
|---|---|
裸 malloc / free |
0 |
裸 strdup (非 host->) |
0 |
裸 new / delete |
0 |
g_host->alloc / g_host->free / g_host->strdup |
所有跨边界分配均通过此路径 |
std::string 跨边界 |
否 — 仅内部使用,不出 DLL |
内部 std::string (PluginConfig, extract_host_port, build_* 等) 均不穿越 DLL 边界。✅
维度 4: §6 原子回调 / 并发安全
评级: C — 与 W11.1 context_plugin 相同模式。
| 变量 | 写入点 | 读取点 | 同步 |
|---|---|---|---|
g_host |
L445 (on_init), L466 (on_shutdown) | L150, L155, L158, L250-253, L291, L298, L413, L425-427 | 无 |
g_http |
L446 (on_init), L464 (on_shutdown) | L269, L286, L345, L367 | 无 |
g_config |
L447 (on_init), L465 (on_shutdown) | 无读取 | 无 |
Race: on_shutdown 置 g_host = nullptr (L466) 与 my_chat 读取 g_host (L291) 无同步。若 host 在 shutdown 期间不保证无 in-flight 调用,则为数据竞争。虽与 codebase 其余部分一致(§6.5 PluginLoader 无内部互斥),但仍是技术债务。
维度 5: SSE 解析鲁棒性 (核心专项)
发现 3 — [中] SSE [DONE] sentinel 精确匹配过脆
- 行号: L208-216 (
parse_sse_line) - 代码:
if (data == "[DONE]")— 精确字符串比较。 - 问题: 若 server 返回
data: [DONE](尾随空格) 或data: [DONE](双空格),sentinel 不匹配 →parse_sse_line返回 false →sse_line_callback返回 1 (continue) → 流永不终止。 - 缓解: OpenAI/DeepSeek 官方实现始终使用精确
data: [DONE],故此问题仅在第三方兼容 server 或未来协议变更时出现。 - 对比 anthropic: anthropic 使用
type == "message_stop"JSON 字段检测,对空格变化免疫。
SSE 解析鲁棒性逐项分析
| 场景 | 行号 | 行为 | 判定 |
|---|---|---|---|
data: 未以 "data: " 开头 |
L210 | rfind 返回非 0 → false, 行被跳过 |
安全 (line 回调继续) |
| JSON 解析失败 (畸形 JSON) | L229-231 | catch(...) 吞异常, 返回 false |
安全 ✅ |
| choices 数组为空 | L222 | choices.empty() → false, 行跳过 |
安全 ✅ |
| choices[0] 无 "delta" key | L223 | obj["delta"] 插入 null, .as_object() 抛异常 → catch |
安全 ✅ |
| delta 无 "content" key | L224 | delta.contains("content") → false |
安全 ✅ |
| data payload 为空 (line="data: ") | L212 | substr(6) → "", == "[DONE]" false, json::parse("") 抛异常 → catch |
安全 ✅ |
| 多字节 UTF-8 在行边界截断 | -- | 依赖 HTTP 层行缓冲; std::string 字节不可知, 不 crash |
低风险 |
[DONE] 带尾随空格 |
L213 | 精确匹配失败 → 流不终止 | 中风险 |
| server 发送非 SSE 行 (event:, id:, retry:) | L210 | rfind("data: ", 0) != 0 → false, 跳过 |
安全 ✅ |
综合评价: catch(...) 提供了良好的兜底保护, 不会因畸形 server 输出 crash。唯一实质性缺陷是 [DONE] 检测脆弱性。
维度 6: JSON 字段缺失/类型错误容错
非流式 parse_response (L137-203)
| 场景 | 行号 | 行为 | 判定 |
|---|---|---|---|
| body 为 nullptr | L146, L167 | body ? body : "{}" → "{}" |
安全 ✅ |
| HTTP 非 2xx + body JSON 无 "error" key | L148 | obj.contains("error") → false, 走 L157 generic error |
安全 ✅ |
| choices 缺失或非数组 | L169 | obj["choices"].as_array()→ 抛异常 → catch |
安全 ✅ |
| choices 为空数组 | L170 | choices.empty() → "empty response" |
安全 ✅ |
| choices[0] 无 "message" | L171 | .as_object() 抛异常 → catch |
安全 ✅ |
| message 无 "content" | L173 | value_to<std::string> 抛异常 → catch |
安全 ✅ |
| error.message 缺失/非字符串 | L150-151 | value_to<std::string> 抛异常 → 外层 catch |
安全 ✅ |
评级: A — 双重 catch (std::exception& + ...) + 合理的 fallback 逻辑。
维度 7: api_key 安全
评级: B+ — 基本安全,有改进空间。
| 检查点 | 行号 | 判定 |
|---|---|---|
| 日志不输出 api_key | L250-253 | ✅ (W9.3 已确认) |
| build_headers_json 结果不进入日志 | L67-72 | ✅ |
| secure_zero 清理 api_key | L462 | ✅ (on_shutdown 调用) |
| 内存中明文存储 | L29 | ⚠️ 全局 std::string 整个会话周期明文存在; 仅 shutdown 时清零 |
| Authorization header 构造 | L70 | "Bearer " + api_key → 临时 string, 无额外泄漏 |
改进方向: 每次 HTTP 请求后立即 secure_zero 临时 buffer,减少明文驻留时间。但此为深度防御,非必须。
维度 8: 与 anthropic 代码重复度
重复度量化
| 类别 | deepseek 行数 | anthropic 对应 | 重复度 |
|---|---|---|---|
| 头文件/namespace | 9 | 9 | 100% |
| 全局指针 (g_host/g_http/g_config) | 3 | 3 | 100% |
| PluginConfig + g_cfg | 9 | 9 | 100% |
| secure_zero | 4 | 4 | 100% |
| extract_host_port | 21 | 21 | 100% |
| configure | 19 | 19 | ~95% (仅 log tag 不同) |
| chat (主流程) | 41 | 41 | ~90% (endpoint path 不同) |
| StreamContext + sse_line_callback | 28 | 28 | ~85% |
| chat_stream (主流程) | 80 | 80 | ~85% |
| free_result | 7 | 7 | 100% |
| g_service vtable | 6 | 6 | 100% 结构 |
| on_init | 15 | 15 | ~95% (service name 不同) |
| on_shutdown | 9 | 9 | ~95% (log tag 不同) |
| g_info + dstalk_plugin_init | 19 | 19 | ~95% |
| 可重构行合计 | ~270 | ~55% |
唯一代码
deepseek 真正独有的代码 (~130 行):
build_headers_json(L67-72): 接收裸 api_key 并组装 Bearer tokenappend_history(L77-97): tool 消息处理 (tool_call_id, tool_calls_json)build_request_json(L102-133): OpenAI chat completions 格式parse_response(L137-203): choices[] 数组格式解析parse_sse_line(L208-233): OpenAI SSE delta 格式
结论
重复度 ~55%。以下组件可抽取为共享基类/模板:
extract_host_port(21 行, 100% 相同)secure_zero(4 行, 100% 相同)PluginConfig+my_configure模式 (~28 行, 95% 相同)my_chat/my_chat_stream主流程骨架 (~120 行, 85% 相同)free_result/g_service/on_init/on_shutdown/g_info/dstalk_plugin_init(~56 行, 95% 相同)
可重构面: ~230 行可提取为 ai_plugin_base 共享库, 各 AI 插件仅需实现 JSON 构建/解析 (~130 行)。
TOP 3 严重问题
-
[严重] L91, L129, L261-L301, L337-L417 — C++ 异常穿越 C ABI 边界 (§8)
my_chat/my_chat_stream调用json::parse(tools_json)和json::parse(tool_calls_json)无 try/catch。畸形 JSON → 异常沿 C 函数指针返回 host →std::terminate()→ 进程崩溃。与 anthropic 同源缺陷。 -
[中] L208-L216 — SSE [DONE] sentinel 精确匹配脆弱
data == "[DONE]"精确比较。尾随空格或格式变化 → sentinel 不识别 → 流不终止 → 调用方 hang。 -
[中] L14-16, L459-L466 — g_host/g_http/g_config 无同步读写
on_shutdown与 service 函数并发访问同组全局指针, 无 atomic/mutex。虽有隐式时序假设, 但属正式数据竞争 (与 codebase 其余部分一致)。
整体评级
| 维度 | 评级 |
|---|---|
| §5+§8 异常安全 | D — 所有 ABI 入口无保护, 畸形输入可 crash 进程 |
| §9 字符串生命周期 | A — 全部模式 A/B, 零违规 |
| §2 跨 DLL 堆纪律 | A — 零违规 |
| §6 并发安全 | C — 与 codebase 一致的数据竞争 |
| SSE 解析鲁棒性 | B — catch(...) 兜底好, [DONE] 检测脆 |
| JSON 容错 | A — 双重 catch + 合理 fallback |
| api_key 安全 | B+ — secure_zero 到位, 明文驻留可优化 |
| 与 anthropic 重复 | D (维护面) — ~55% 重复, ~230 行可抽取 |
| 综合 | C+ |
总评: SSE 解析因为有 catch(...) 全面兜底, 比预期更鲁棒。核心风险在于所有 ABI 入口函数无 try/catch — 一旦传入畸形 tools_json 或 tool_calls_json, JSON 解析异常直接导致进程 std::terminate()。这是可稳定复现的 crash 路径, 非理论威胁。与 anthropic 的 ~55% 重复度表明存在显著"可重构面", 建议后续 Wave 考虑抽取 ai_plugin_base 共享层。
Findings Summary
| ID | Severity | Title | Fix Wave |
|---|---|---|---|
| F-13.2-1 | HIGH | C++ exceptions cross C ABI boundary (§8): json::parse(tools_json) in build_request_json (L129) and json::parse(tool_calls_json) in append_history (L91) can throw → std::terminate() | W14 |
| F-13.2-2 | MEDIUM | Asymmetric exception protection: parse_response has internal try/catch but build_request_json does not (L129 json::parse unprotected); caller my_chat/my_chat_stream also lack wrapping | W14 |
| F-13.2-3 | MEDIUM | SSE [DONE] sentinel exact match too brittle (L213): trailing spaces or format deviation prevent match → stream never terminates → caller hang | -- |
| F-13.2-4 | MEDIUM | 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 | -- |