Wave 8: tech-debt audits, core unit tests, CLI pipe input (W11.1-W11.7)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

- 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>
This commit is contained in:
2026-05-27 09:06:25 +08:00
parent 004a81db96
commit bb2e8c0220
14 changed files with 1122 additions and 18 deletions

View File

@@ -0,0 +1,164 @@
# 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_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), 静态 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_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 是静态字符串数组, 无需释放, 无问题。