Files
dstalk/agents/audits/W13.2-deepseek-audit.md
XiuChengWu 6f492489c6
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
W16: close CRITICAL/HIGH findings, integrate metadata gate, complete audit summaries (W16.1-W16.6)
- 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>
2026-05-27 18:45:03 +08:00

13 KiB
Raw Blame History

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_jsonjson::parse(tools_json), append_historyjson::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 → L129 json::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 在 L129 json::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_shutdowng_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 token
  • append_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%。以下组件可抽取为共享基类/模板:

  1. extract_host_port (21 行, 100% 相同)
  2. secure_zero (4 行, 100% 相同)
  3. PluginConfig + my_configure 模式 (~28 行, 95% 相同)
  4. my_chat / my_chat_stream 主流程骨架 (~120 行, 85% 相同)
  5. free_result / g_service / on_init / on_shutdown / g_info / dstalk_plugin_init (~56 行, 95% 相同)

可重构面: ~230 行可提取为 ai_plugin_base 共享库, 各 AI 插件仅需实现 JSON 构建/解析 (~130 行)。


TOP 3 严重问题

  1. [严重] 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 同源缺陷。

  2. [中] L208-L216 — SSE [DONE] sentinel 精确匹配脆弱 data == "[DONE]" 精确比较。尾随空格或格式变化 → sentinel 不识别 → 流不终止 → 调用方 hang。

  3. [中] 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 --