- 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>
274 lines
16 KiB
Markdown
274 lines
16 KiB
Markdown
# W13.1 Anthropic Plugin Audit
|
||
|
||
**Auditor**: 杨帆 (architect-yang)
|
||
**Date**: 2026-05-27
|
||
**File**: plugins/anthropic/src/anthropic_plugin.cpp (497 行)
|
||
**Wave Coverage**: W2.2 (仅 secure_zero 追加,全文从未审计)
|
||
|
||
---
|
||
|
||
## 文件总览
|
||
|
||
| 函数 | 行号 | 类型 | 用途 |
|
||
|------|------|------|------|
|
||
| `secure_zero` | 34-37 | 静态辅助 | volatile 写零擦除敏感数据 |
|
||
| `extract_host_port` | 42-62 | 静态辅助 | URL 解析 host/port/target |
|
||
| `build_headers_json` | 67-73 | 静态辅助 | 构建 Anthropic headers JSON (含 api_key) |
|
||
| `build_request_json` | 78-124 | 静态辅助 | 构建 Messages API 请求体 |
|
||
| `parse_response` | 129-195 | 静态辅助 | 解析非流式 JSON 响应 |
|
||
| `parse_sse_data` | 203-238 | 静态辅助 | 解析 SSE data 行 (Anthropic 格式) |
|
||
| **`my_configure`** | **243-261** | **vtable** | AI 服务 configure |
|
||
| **`my_chat`** | **266-306** | **vtable** | 非流式聊天 |
|
||
| **`my_chat_stream`** | **348-428** | **vtable** | 流式聊天 (SSE) |
|
||
| `sse_line_callback` | 321-346 | 回调 (C fn ptr) | SSE 行回调,传给 HTTP 服务 |
|
||
| **`my_free_result`** | **433-439** | **vtable** | 释放 chat_result 字段 |
|
||
| **`on_init`** | **454-468** | **生命周期** | 插件初始化 |
|
||
| **`on_shutdown`** | **470-478** | **生命周期** | 插件销毁 |
|
||
| `dstalk_plugin_init` | 494-497 | **导出入口** | 返回插件描述符 |
|
||
|
||
**vtable 绑定**: `g_service` (L444-449) 绑定 configure/chat/chat_stream/free_result。
|
||
**全局状态**: `g_host` (L14), `g_http` (L15), `g_config` (L16), `g_cfg` (L29)。
|
||
|
||
---
|
||
|
||
## 1. 审计维度: §5 + §8 异常安全
|
||
|
||
**规则 (§8.1)**: 所有 C ABI 导出函数(on_init/on_shutdown/on_event + vtable 函数指针)必须 try/catch 包裹。
|
||
|
||
### 发现 1.1 — [H] 6 个 C ABI 函数零 try/catch 保护
|
||
|
||
以下所有通过 vtable 或 `dstalk_plugin_info_t` 暴露给 host 的函数,内部使用 `std::string` / `boost::json` / STL,但**无一包裹 try/catch**:
|
||
|
||
| 函数 | 行号 | 使用的可抛类型 | 风险 |
|
||
|------|------|---------------|------|
|
||
| `my_configure` | L243-261 | `std::string::operator=` (L247-250) | `std::bad_alloc` → `std::terminate()` |
|
||
| `my_chat` | L266-306 | `std::string` x5 (L279-286) + `boost::json` (build 系列) | 同上 |
|
||
| `my_chat_stream` | L348-428 | `std::string` x4 (L361-368) + `boost::json` | 同上 |
|
||
| `on_init` | L454-468 | `g_host->log` 可变参数 + query_service | 低概率但可能 |
|
||
| `on_shutdown` | L470-478 | `std::string::clear()` (noexcept) + `g_host->log` | 极低概率 |
|
||
| `sse_line_callback` | L321-346 | `std::string line_str(line)` (L326) | `std::bad_alloc` 抛向调用方 (网络插件) |
|
||
|
||
**违反 §8.1 条目 1+2+4**: on_init/on_shutdown + vtable 函数指针 + register_service 注册的函数。
|
||
**代价**: 任何 OOM 或 STL 异常 → 穿越 C 函数指针边界 → `std::terminate()` → 进程崩溃。
|
||
|
||
W11.1 对 context_plugin 发现了完全相同的模式 (trim_impl),本插件问题范围更广(6 个函数 vs 1 个)。
|
||
|
||
**对比 deepseek_plugin.cpp**: deepseek 同函数**同样零 try/catch**(my_configure L238-256, my_chat L261-301, my_chat_stream L337-417, on_init L443-457, sse_line_callback L317-335),说明此反模式是项目级共性问题。
|
||
|
||
### 发现 1.2 — [M] sse_line_callback 无保护但可能被网络插件兜底
|
||
|
||
L326 `std::string line_str(line)` 可抛 `std::bad_alloc`。回调通过 C 函数指针 `dstalk_stream_cb` 传入网络插件的 `post_stream`。根据 security-logging.md 对 network_plugin 的描述(L280-282 有 `catch (std::exception& e)`),网络插件的 do_post_stream 可能兜底捕获。但依赖调用方的异常保护是脆弱假设——若网络插件变更或不同版本,此防护消失。
|
||
|
||
---
|
||
|
||
## 2. 审计维度: §9 字符串返回值生命周期
|
||
|
||
**规则 (§9)**: vtable 中返回 `const char*` 的函数必须符合模式 A(拥有权转移,host 堆分配 + 调用方 host->free)或模式 B(静态生命周期)。
|
||
|
||
### 发现 2.1 — [L] 临时 std::string + c_str() + strdup 的脆弱模式
|
||
|
||
L405-406:
|
||
```c
|
||
r.error = g_host->strdup(("HTTP " + std::to_string(status_code)).c_str());
|
||
```
|
||
临时 `std::string` 在完整表达式结束前有效,`strdup` 在其析构前完成复制。**当前安全**,但重构时若分离 `c_str()` 和 `strdup` 调用会导致悬垂指针。Deepseek 同位置 (L393-395) 使用相同模式。
|
||
|
||
### 合规确认
|
||
|
||
| 检查项 | 状态 | 证据 |
|
||
|--------|------|------|
|
||
| chat_result 字段全部经 strdup 分配 | PASS | L144, L146, L150, L168, L176, L179, L186, L191, L275, L296, L357, L396-397, L403, L405-406, L418, L424 |
|
||
| free_result 全部经 host->free 释放 | PASS | L436-438 |
|
||
| 无锁外 c_str() 返回 | PASS | 插件无 mutex/lock |
|
||
| 无栈上 buffer 返回 | PASS | 全部 strdup 复制 |
|
||
| 无 `std::string{...}.c_str()` 裸返回 | PASS | 临时对象均在 strdup 前存活 |
|
||
|
||
---
|
||
|
||
## 3. 审计维度: §2 + §3 跨 DLL 堆纪律
|
||
|
||
**规则 (§3.2)**: 严禁直接调用 malloc/free/strdup/new/delete 处理跨 DLL 边界数据。
|
||
|
||
### 发现 3.1 — [H] response_body 泄漏:my_chat 错误路径未释放
|
||
|
||
L291-297 (`my_chat`):
|
||
```c
|
||
int ret = g_http->post_json(..., &response_body, &status_code);
|
||
if (ret != 0) {
|
||
r.error = g_host->strdup("http request failed");
|
||
return r; // ← response_body 未释放!
|
||
}
|
||
```
|
||
成功路径 (L302-304) 正确释放了 `response_body`,但 `ret != 0` 错误路径泄漏。
|
||
|
||
**对比**: `my_chat_stream` (L408, L414) 在**所有路径**正确释放 `response_body`。`my_chat` 应与 `my_chat_stream` 保持一致。
|
||
|
||
**deepseek_plugin.cpp 同位置** (L286-293) 有相同泄漏——此为项目级模式 bug。
|
||
|
||
### 合规确认
|
||
|
||
| 检查项 | 状态 | 证据 |
|
||
|--------|------|------|
|
||
| 无裸 malloc/free | PASS | 全文 0 处 |
|
||
| 无裸 strdup | PASS | 全文 0 处 |
|
||
| 无裸 new/delete | PASS | 全文 0 处 |
|
||
| 跨边界分配均经 host->strdup | PASS | 全部 chat_result 字段 |
|
||
| 跨边界释放均经 host->free | PASS | L302-304, L408, L414, L436-438 |
|
||
| 插件内部 std::string 不出边界 | PASS | 仅用于局部逻辑 |
|
||
|
||
---
|
||
|
||
## 4. 审计维度: §6 atomic 回调 + 线程安全
|
||
|
||
**规则 (§6.1)**: 诊断回调用 `std::atomic` + acquire/release。服务注册表/事件总线均有内部锁。
|
||
|
||
### 发现 4.1 — [H] 全局指针无同步保护 (g_host / g_http / g_config)
|
||
|
||
| 变量 | 写点 (on_shutdown) | 读点 (服务函数) | 同步机制 |
|
||
|------|-------------------|-----------------|----------|
|
||
| `g_host` | L477 `= nullptr` | L255, L274, L296, L357, L436 等 | **无** |
|
||
| `g_http` | L475 `= nullptr` | L274, L356 | **无** |
|
||
| `g_config` | L476 `= nullptr` | (未读取,死变量) | **无** |
|
||
|
||
**Race 场景**: 线程 T1 在 `my_chat()` L274 读取 `g_http`(非 null),线程 T2 在 `on_shutdown()` L475 写入 `nullptr`。T1 随后 L291 调用 `g_http->post_json(...)` → **空指针解引用**。
|
||
|
||
**缓解因素**: host 应保证 shutdown 前无 in-flight 调用,但 ABI 未显式保证。与 W11.1 发现的 context_plugin Race A 问题同根。
|
||
|
||
### 发现 4.2 — [L] g_config 死变量
|
||
|
||
L16 声明 `g_config`,L458 赋值,L476 置 null。全文无任何读取点。不造成泄漏但暗示设计与实现脱节。
|
||
|
||
### 发现 4.3 — [PASS] 流式回调路径无需 atomic
|
||
|
||
`StreamContext` (L312-318) 在 `my_chat_stream` 栈上创建 (L370),回调 `sse_line_callback` 在 `post_stream` 同步调用期间使用,无多线程竞争。
|
||
|
||
---
|
||
|
||
## 5. 审计维度: api_key 安全
|
||
|
||
### 发现 5.1 — [PASS] on_shutdown 正确 secure_zero
|
||
|
||
L473-474:
|
||
```c
|
||
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
|
||
g_cfg.api_key.clear();
|
||
```
|
||
W2.2 修复已验证生效:先 volatile 零覆盖,再 clear 释放。`secure_zero` (L34-37) 使用 `volatile char*` 写入循环,是标准可移植的编译器优化防护手段。
|
||
|
||
### 发现 5.2 — [PASS] 日志不泄漏 api_key
|
||
|
||
W9.3 已审计确认:`my_configure` L255-258 的日志调用有意排除 `api_key`,仅输出 model/base_url/max_tokens/temperature。`build_headers_json` (L67-73) 构建的 headers JSON 仅通过内存传递至 HTTP 请求,不进入日志管道。
|
||
|
||
### 发现 5.3 — [L] 堆内存残留
|
||
|
||
`build_headers_json` 返回的 `std::string` 含 `x-api-key: sk-...`,在 `my_chat`/`my_chat_stream` 栈上存活至函数返回。RAII 析构释放内存但不擦除。攻击者读取进程堆可发现 api_key 残留。这是所有内存中处理凭证的固有限制,非本插件特有,secure_zero 仅覆盖 `g_cfg.api_key` 主副本。
|
||
|
||
---
|
||
|
||
## 6. 审计维度: 错误处理路径
|
||
|
||
### 发现 6.1 — [L] my_chat 对 post_json 错误仅返回通用消息
|
||
|
||
L291-297: `ret != 0` 时仅返回 "http request failed",未利用 `status_code` 区分超时/SSL/DNS 错误。`my_chat_stream` (L402-406) 有更细粒度的错误消息(区分 `status_code <= 0` 为 "transport error")。
|
||
|
||
### 发现 6.2 — [PASS] 空响应/空 body 不崩溃
|
||
|
||
- `parse_response` L136/L158: `body ? body : "{}"` 保护 null 输入
|
||
- L177-179: content 数组为空 → "empty response"
|
||
- `my_chat_stream` L416-418: `accumulated.empty() && !saw_data_line` → "no content received"
|
||
- `parse_sse_data` L234: catch(...) 静默忽略 JSON 解析失败
|
||
|
||
### 发现 6.3 — [PASS] HTTP 错误码正确传播
|
||
|
||
`parse_response` L134-154: 非 2xx 状态码提取 error.message 或退化为 "HTTP NNN"。
|
||
`my_chat_stream` L388-411: 同模式,附加 `status_code <= 0` 的传输错误处理。
|
||
|
||
---
|
||
|
||
## 7. 审计维度: 资源生命周期
|
||
|
||
### 发现 7.1 — [PASS] 流式中断资源正确释放
|
||
|
||
- `StreamContext` (L312-318) 栈分配,RAII 保证析构
|
||
- `response_body` 在 `my_chat_stream` L408 + L414 双路径释放
|
||
- `user_cb` 返回 0 时(L340)`sse_line_callback` 返回 0,`post_stream` 应据此停止——无孤儿连接
|
||
- 流结束后 `ctx.accumulated` 正确取出赋值 L424
|
||
|
||
### 发现 7.2 — [L] my_chat_stream 忽略 post_stream 返回值
|
||
|
||
L379-383: `int ret = g_http->post_stream(...)` 的结果未被检查。`my_chat` 检查了 `ret`(L295),但 `my_chat_stream` 仅在 status_code 上做判断。若 `ret != 0` 但 `status_code == 0`(连接完全失败),会落入 L402 `status_code <= 0` → "transport error" 分支——实际可工作,但不一致。
|
||
|
||
---
|
||
|
||
## 8. 补充发现
|
||
|
||
### 8.1 — [L] Anthropic 特有: tool_use 块被静默忽略
|
||
|
||
`parse_response` L163-173 仅提取 `type == "text"` 的 content block。Anthropic API 在 tool-use 场景下返回 `type == "tool_use"` 的块,其内容不会被提取到 `r.content`,`r.tool_calls_json` 始终被设为 `nullptr` (L153, L171, L182, L187, L193)。调用方期待 tool_calls 时将收到空 content + 空 tool_calls——静默数据丢失。不过 `dstalk_ai_service_t.chat` 签名设有 `tools_json` 入参但未在 Anthropic 请求体中设置(对比 deepseek L129 有 `root["tools"] = json::parse(tools_json)`),因此当前工具调用完全不可用。
|
||
|
||
### 8.2 — [L] system 消息合并到顶层字段
|
||
|
||
L92-97: Anthropic API 要求 system 为顶层字段(非 messages 数组元素),插件正确实现了此分离。但多段 system 消息用 `"\n\n"` 拼接 (L95),跨消息语义边界可能模糊。
|
||
|
||
---
|
||
|
||
## TOP 3 严重问题
|
||
|
||
### 发现 1 — [H] 6 个 C ABI 函数零 try/catch (§8 违反正面清单)
|
||
**行号**: L243 (my_configure), L266 (my_chat), L348 (my_chat_stream), L321 (sse_line_callback), L454 (on_init), L470 (on_shutdown)
|
||
**问题**: 全部使用 std::string/boost::json 但无异常保护。任何 std::bad_alloc → std::terminate()。
|
||
**修复**: 每个函数外层加 try { ... } catch (const std::exception& e) { g_host->log(ERROR, ...); return 错误码; } catch (...) { return 错误码; }
|
||
|
||
### 发现 2 — [H] response_body 泄漏 + 全局指针无同步
|
||
**行号**: L295-297 (泄漏), L14-L16 vs L475-L477 (竞态)
|
||
**问题**: (a) my_chat 在 ret!=0 时未释放 response_body(my_chat_stream 正确);(b) g_host/g_http/g_config 无原子/互斥保护,on_shutdown 与 服务函数并发可空指针解引用。
|
||
**修复**: (a) 在 L296 前加 `if (response_body) g_host->free(response_body);`;(b) g_host/g_http 用 `std::atomic` 或文档化单线程约定。
|
||
|
||
### 发现 3 — [M] Anthropic tool_use 响应静默丢弃
|
||
**行号**: L129-195 (parse_response), L78-124 (build_request_json)
|
||
**问题**: (a) build_request_json 未将 tools_json 参数写入请求体(对比 deepseek L128-130);(b) parse_response 仅提取 text block,tool_use 块被忽略,tool_calls_json 始终 nullptr。用户调用工具时得到空响应无错误提示。
|
||
|
||
---
|
||
|
||
## 整体评级: C
|
||
|
||
| 维度 | 评级 | 理由 |
|
||
|------|------|------|
|
||
| §8 异常安全 | **F** | 6 个函数零 try/catch——mandatory 规则完全未满足 |
|
||
| §2/§3 跨 DLL 堆 | **A** | 无裸 malloc/free/strdup,全部经 host API |
|
||
| §9 字符串生命周期 | **B+** | 全部 strdup 合规,一处临时+strdup 脆弱模式 |
|
||
| §6 线程安全 | **C** | 全局指针无同步,与 W11.1 同根问题 |
|
||
| api_key 安全 | **A** | secure_zero 生效,日志无泄漏 |
|
||
| 错误处理 | **C+** | 空响应不崩溃,但 response_body 泄漏+返回值忽略 |
|
||
| 资源生命周期 | **B+** | 流式路径 RAII 干净,仅 my_chat 泄漏点 |
|
||
| **综合** | **C** | 堆纪律意外干净,但异常安全零覆盖 + 全局指针竞态 + response_body 泄漏 将评级拉至 C |
|
||
|
||
**总评**: anthropic_plugin 在跨 DLL 堆纪律方面表现好(与 context_plugin 一致),说明编码者遵循了正确的内存管理模式。但 **§8 异常安全是强制性要求,当前零覆盖在最坏情况下直接导致进程崩溃**。response_body 泄漏和全局指针竞态进一步降低可靠性。tool_use 静默丢弃虽非安全/稳定性问题,但构成功能缺陷。评级 C,建议在下一 Wave 优先修复发现 1 和发现 2。
|
||
|
||
---
|
||
|
||
## 审计元数据
|
||
|
||
- **审计标准**: plugin-abi.md v1.1 (含 §8 异常安全 + §9 字符串生命周期,W12.6 追加)
|
||
- **参考审计风格**: agents/audits/W11.1-context-audit.md
|
||
- **安全日志参考**: docs/explanation/security-logging.md (W9.3)
|
||
- **对比文件**: plugins/deepseek/src/deepseek_plugin.cpp (仅参考,不审计)
|
||
- **不修改文件**: anthropic_plugin.cpp (审计只读)
|
||
|
||
|
||
## Findings Summary
|
||
|
||
| ID | Severity | Title | Fix Wave |
|
||
|----|----------|-------|----------|
|
||
| F-13.1-1 | HIGH | 6 C ABI functions zero try/catch protection (§8): my_configure (L243), my_chat (L266), my_chat_stream (L348), sse_line_callback (L321), on_init (L454), on_shutdown (L470) -- any std::bad_alloc → std::terminate() | W14 |
|
||
| F-13.1-2 | HIGH | response_body leak in my_chat error path (L295-297): ret!=0 returns without freeing response_body (my_chat_stream correctly frees it) | -- |
|
||
| F-13.1-3 | HIGH | g_host/g_http/g_config global pointers no sync protection (L14-16 vs L475-L477): on_shutdown nullptr write races with service function reads | -- |
|
||
| F-13.1-4 | MEDIUM | sse_line_callback no exception protection (L326 std::string alloc via C fn ptr): relies on network plugin's try/catch as fragile assumption | W14 |
|
||
| F-13.1-5 | LOW | temporary std::string + c_str() + strdup fragile pattern (L405-406): safe today but refactoring risk if c_str/strdup calls separated | -- |
|
||
| F-13.1-6 | LOW | g_config dead variable (L16): written in on_init (L458) and on_shutdown (L476), never read | -- |
|
||
| F-13.1-7 | LOW | heap memory residual for api_key after RAII destruction: build_headers_json returns std::string with x-api-key on stack, not zeroed on free | -- |
|
||
| F-13.1-8 | LOW | my_chat post_json error returns only generic "http request failed" (L295-297): does not distinguish timeout/SSL/DNS | -- |
|
||
| F-13.1-9 | LOW | my_chat_stream ignores post_stream return value (L379-383): only checks status_code, not ret | -- |
|
||
| F-13.1-10 | LOW | Anthropic tool_use blocks silently ignored (L163-173): parse_response only extracts type=="text", tool_use blocks lost; tool_calls_json always nullptr | -- |
|
||
| F-13.1-11 | LOW | system messages merged with "\n\n" (L95): may blur cross-message semantic boundaries | -- |
|