# 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` → 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_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` 抛异常 → catch | 安全 ✅ | | error.message 缺失/非字符串 | L150-151 | `value_to` 抛异常 → 外层 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` 共享层。