- 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>
243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# 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<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 | -- |
|