- W11.1 context_plugin audit (architect-huang): 3 findings on ABI exception safety, strdup null checks, dead g_max_tokens variable. Rating: B. - W11.2 config audit (engineer-chen): identified 74-line TOML parser duplication between config_plugin and config_store, dual-store data isolation, dangling c_str() risk. Rating: C. - W11.3 event_bus + service_registry unit tests (qa-liu): 12 cases total, ctest coverage 2 -> 4 targets, 100% pass. - W11.4 CLI stdin pipe mode (engineer-zhao): isatty detection, single-shot inference path with exit codes 0/1/2/3. - W11.6 scripts/refresh_status.py (engineer-li): 431-line generator that scans 16 profile.md + 5 group.md to regenerate STATUS.md. - W11.7 destructive testing (qa-xu): 10 input scenarios PASS, found bin copy mismatch (BUG-1) plus 3 minor UX bugs for follow-up. Verified: cmake build 0 error, ctest 4/4 pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.3 KiB
W11.1 Context Plugin Audit
Auditor: 黄岭 (architect-huang) Date: 2026-05-27 File: plugins/context/src/context_plugin.cpp (289 行) Wave Coverage: 零 (从未被 Wave 流程审计)
1. 跨 DLL 堆合规性
状态: 合规 ✅
逐行检查所有 malloc/free/strdup/new/delete 调用点:
| 调用类型 | 搜索结果 | 判定 |
|---|---|---|
malloc() |
0 处 | -- |
free() |
0 处 | -- |
strdup() (裸) |
0 处 | -- |
new (显式) |
0 处 | -- |
delete (显式) |
0 处 | -- |
g_host->alloc() |
L135, L215 | 正确: host 堆分配 |
g_host->strdup() |
L138-141, L219-222 | 正确: host 堆分配 |
std::string / std::vector |
L90-127 等多处 | 插件内部使用, 不跨边界 |
结论: 所有跨 DLL 边界的分配均通过 g_host->alloc() / g_host->strdup(), 符合 §3.2 硬性规则。内部 C++ 类型 (std::string/std::vector) 仅用于 TrimMessage 局部数据, 不出 DLL 边界。W2.1 堆纪律已隐式合规, 无需迁移。
注意: L159/L168 使用 std::fprintf(stderr, ...), 非内存操作, 无堆问题。
2. ABI 契约符合度 (plugin-abi.md §1-§7)
§1 DSTALK_API_VERSION
- L280:
DSTALK_API_VERSION✅ - 导出函数签名
dstalk_plugin_info_t* dstalk_plugin_init(void)与dstalk_plugin_init_fntypedef 匹配 ✅
§2 内存所有权
g_info.name/version/description为静态字符串字面量, 生命周期覆盖 init_fn 调用 ✅ (§2.2)- 输出数组通过
g_host->alloc+g_host->strdup分配, 调用方负责g_host->free✅ (§2.3)
§3 跨 DLL 堆
- 已在上节验证 ✅
§4 register_service
- 仅
on_init内调用 (L268) ✅ (§4.1) - 版本号 1 ✅
§5 on_init/on_shutdown
-
函数签名匹配:
int (*)(const dstalk_host_api_t*)/void (*)(void)✅ -
违反 §5.3 (异常安全):
on_init/context_count_tokens/context_trim/context_set_max_tokens均定义为 C 函数指针, 但底层 C++ 实现 (trim_implL114-226) 使用了std::vector/std::string, 二者均可抛出std::bad_alloc。无任何try/catch包裹。若 OOM, 异常穿越 C ABI 边界 →std::terminate(), 违反 §5.3 约束。受影响函数:
context_trim→trim_impl(L114),on_init→g_host->log(可变参数, 低风险)
§6 线程安全
- 见第 4 节
§7 依赖声明
- L281:
{"session", nullptr, ...}, NULL 终止 ✅ on_init内通过host->query_service("session", 1)验证依赖可用 (L261-265) ✅
评级: 基本合规, 但因 §5.3 异常安全缺失降为 B
3. 内存/资源泄漏分析
3.1 服务注册
on_init注册 "context" 服务 (L268), 静态 vtableg_context_service无动态分配。on_shutdown(L271-274) 未调用unregister_service— 但合规: 按 §4.4, host 在插件卸载时自动调用ServiceRegistry::unregister_service。无需手动清理。
3.2 事件订阅
- 无事件订阅, 无泄漏风险 ✅
3.3 trim_impl 内存分配路径
三条返回路径分析:
| 路径 | 位置 | 内存状态 |
|---|---|---|
| 单条消息超限, 提前返回 | L172-174 | *out=nullptr, 无分配, 局部 vector RAII 清理 ✅ |
| 完整副本路径 (current ≤ max) | L133-144 | alloc + strdup x N, 全部返回给调用方 ✅ |
| 裁剪后路径 | L213-225 | alloc + strdup x N, 全部返回给调用方 ✅ |
发现: g_host->strdup (L138-141, L219-222) 返回值未被检查。若 strdup 在循环中途失败返回 nullptr:
- 前置已成功的 strdup 分配被泄漏 (无记录, 无法逐个释放)
- nullptr 被静默存入 message field, 函数返回 0 (成功)
- 调用方解引用 nullptr 的 role/content → 崩溃
结论: 正常路径无泄漏, 但 OOM 路径存在资源泄漏 + 空指针隐患。
3.4 g_session 死引用
- L261-266:
query_service("session")获取并存入g_session - 全文搜索:
g_session从未被读取 (仅在 L272on_shutdown中置 null) - 不造成泄漏 (无额外分配), 但 dead code 暗示设计与实现脱节
评级: B (正常路径干净, OOM 路径脆弱)
4. 并发安全分析
4.1 共享状态清单
| 变量 | 写入点 | 读取点 | 同步 |
|---|---|---|---|
g_host |
L258 (on_init), L273 (on_shutdown) | L135/215 (trim_impl → alloc), L138-141/219-222 (strdup) | 无 |
g_session |
L266 (on_init), L272 (on_shutdown) | 无读取 | 无 |
g_max_tokens |
L244 (set_max_tokens) | 无读取 | 无 |
4.2 潜在竞争
Race A: on_shutdown vs. trim_impl 并发 (g_host)
- 线程 T1:
context_trim()→trim_impl()读取g_host(非 null) - 线程 T2:
on_shutdown()写入g_host = nullptr - T1 随后调用
g_host->alloc()→ 空指针解引用 - 缓解因素: host 应保证 shutdown 前无 in-flight 服务调用。但 ABI 未显式保证, 属未定义行为边界。
Race B: set_max_tokens vs. 未来的 getter (g_max_tokens)
- 当前无读取点, 不构成实际竞争。但
set_max_tokens作为公开 API, 未来添加 getter 时需std::atomic保护。当前为潜在债务。
评级: C (g_host 读写的无同步访问是数据竞争, 虽有隐式时序假设但不可依赖)
5. 最严重发现 (Top 3)
发现 1 — [严重] C++ 异常穿越 ABI 边界 (违反 §5.3)
- 行号: L114-226 (trim_impl), L257-268 (on_init), L232-240 (vtable 函数)
- 问题:
trim_impl使用std::vector/std::string可抛std::bad_alloc, 但无try/catch。异常通过 C 函数指针边界 →std::terminate()→ 进程崩溃。违反 plugin-abi §5.3 硬性约束。 - 修复方向: 在
trim_impl体和on_init体加try { ... } catch (...) { return -1; }, 或预分配 + nothrow 版本。
发现 2 — [高] strdup 返回值未检查, OOM 时静默失败 + 泄漏
- 行号: L138-141 (完整副本路径), L219-222 (裁剪后路径)
- 问题: 循环内连续 4 次
g_host->strdup调用均未检查返回值。若某次返回 nullptr: (a) 前置成功的 strdup 分配泄漏 (无释放手段); (b) nullptr 存入 message field, 函数返回 0 (成功); (c) 调用方解引用 null role/content → 段错误。 - 修复方向: 每个 strdup 后检查返回值, 失败时逆序释放已分配字段, 再
g_host->free(*out), 然后返回 -1。
发现 3 — [中] context_set_max_tokens 是死 API (g_max_tokens 未被读取)
- 行号: L21 (声明), L243-244 (写入)
- 问题:
context_set_max_tokens()写入g_max_tokens, 但全文无任何代码路径读取该变量。trim_impl使用参数max_tokens(调用方直接传入), 不依赖全局状态。因此set_max_tokens调用对行为零影响——调用方期待的限制永不生效。 - 修复方向: 要么在
context_trim中读取g_max_tokens作为默认值 (当调用方传 0 时), 要么删除该 API / 改为返回当前值。
6. 整体评级
| 维度 | 评级 |
|---|---|
| 跨 DLL 堆合规 | A (完全合规) |
| ABI 契约符合度 | B (核心合规, §5.3 异常安全缺失) |
| 内存/资源泄漏 | B (正常路径干净, OOM 脆弱) |
| 并发安全 | C (g_host 无同步访问, 隐式时序依赖) |
| 综合 | B |
总评: context_plugin 在堆纪律上意外地干净 (没有需要迁移的裸 malloc/free/strdup/new/delete), 说明编码者即使未读 plugin-abi 也遵循了正确模式。主要风险集中在异常安全 (可导致进程崩溃) 和 OOM 鲁棒性。无安全漏洞、无明确内存泄漏、无崩溃性逻辑错误。
7. 补充发现 (优先级低)
- UTF-8 解码无越界保护 (L42-64, L96-104): 多字节序列 (2/3/4 字节) 的
i += N假设后续字节有效, 若输入截断则读越界。对 token 估算影响极小 (仅计数值偏差), 非功能性 bug。 - token 计数逻辑重复 (L34-68 vs L91-106):
count_tokens_one_message与count_tokens_trim有 ~90% 重复, 在 C 字符串和 std::string 上各实现一套。维护风险 (两端需同步修改), 但不影响正确性。 - 0xC0/0xC1 过短编码未识别 (L52, L100): UTF-8 标准中 0xC0/0xC1 是无效起始字节 (过短编码), 但仍计入
other_chars。仅影响 token 估算计数, 不影响功能。 - on_shutdown 不释放 g_info.dependencies (L281): dependencies 是静态字符串数组, 无需释放, 无问题。