Files
dstalk/agents/audits/W11.1-context-audit.md
XiuChengWu bb2e8c0220
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
Wave 8: tech-debt audits, core unit tests, CLI pipe input (W11.1-W11.7)
- 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>
2026-05-27 09:06:25 +08:00

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_fn typedef 匹配

§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_impl L114-226) 使用了 std::vector / std::string, 二者均可抛出 std::bad_alloc。无任何 try/catch 包裹。若 OOM, 异常穿越 C ABI 边界 → std::terminate(), 违反 §5.3 约束。

    受影响函数: context_trimtrim_impl (L114), on_initg_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), 静态 vtable g_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 从未被读取 (仅在 L272 on_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_messagecount_tokens_trim 有 ~90% 重复, 在 C 字符串和 std::string 上各实现一套。维护风险 (两端需同步修改), 但不影响正确性。
  • 0xC0/0xC1 过短编码未识别 (L52, L100): UTF-8 标准中 0xC0/0xC1 是无效起始字节 (过短编码), 但仍计入 other_chars。仅影响 token 估算计数, 不影响功能。
  • on_shutdown 不释放 g_info.dependencies (L281): dependencies 是静态字符串数组, 无需释放, 无问题。