- 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>
16 KiB
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:
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):
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:
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_responseL136/L158:body ? body : "{}"保护 null 输入- L177-179: content 数组为空 → "empty response"
my_chat_streamL416-418:accumulated.empty() && !saw_data_line→ "no content received"parse_sse_dataL234: 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_streamL408 + 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 | -- |