Files
dstalk/agents/audits/W13.1-anthropic-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

16 KiB
Raw Permalink Blame History

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_allocstd::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/catchmy_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_bodymy_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_configL458 赋值L476 置 null。全文无任何读取点。不造成泄漏但暗示设计与实现脱节。

发现 4.3 — [PASS] 流式回调路径无需 atomic

StreamContext (L312-318) 在 my_chat_stream 栈上创建 (L370),回调 sse_line_callbackpost_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::stringx-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_bodymy_chat_stream L408 + L414 双路径释放
  • user_cb 返回 0 时L340sse_line_callback 返回 0post_stream 应据此停止——无孤儿连接
  • 流结束后 ctx.accumulated 正确取出赋值 L424

发现 7.2 — [L] my_chat_stream 忽略 post_stream 返回值

L379-383: int ret = g_http->post_stream(...) 的结果未被检查。my_chat 检查了 retL295my_chat_stream 仅在 status_code 上做判断。若 ret != 0status_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.contentr.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_bodymy_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 blocktool_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 --