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

243 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | -- |