Skip to content

Chaos Engineering

AceGuru-mjh edited this page Oct 1, 2026 · 4 revisions

混沌工程与崩溃审计

🛠️ 工程化 · 🏠 首页 › Chaos-Engineering

Home Version Kotlin Modules Tools License

Chaos-Engineering typing

📑 本页目录

本项目有两次"混沌工程式"全仓审查(2026-02 评审、2026-09 审计),共落地 24 处修复。 这些是最值得读的"为什么这里这么写"清单。来源:docs/chaos-engineering-review.md、 docs/CHAOS_AUDIT_2026-09.md。

1. 方法与范围

维度 内容
范围 全仓库 20 个 Gradle 模块(含 ComposeFoundry)
方法 全量 grep + 逐行人工核对(行号基于 main@c3176ed)
三维 ① 生命周期竞态 ② 数据一致性与边界值陷阱 ③ 隐式依赖与版本兼容性
规模 评审 12 项(P0×4 / P1×4 / P2×4);审计落地 24 处修复,全部经 CI 同款 kotlinc 2.0.21 验证

2. P0:必现崩溃 / 数据损坏

# 问题 机制 修复
P0-1 VIBRATE 权限缺失 → Agent 进 WAITING_HUMAN 时 triggerVibration() 在 mainHandler.post 抛 SecurityException,Android 8~15 必现进程崩溃 Manifest 未声明权限 + vibrator.vibrate(VibrationEffect.createOneShot(150, DEFAULT_AMPLITUDE)) 无 try/catch 补权限 + ContextCompat.checkSelfPermission 自防御 + runCatching(防 ODM ROM 二次限制)
P0-2 压缩切在 tool_call / result 之间 → 保留窗首段出现无主 role:"tool" → OpenAI 兼容端点 400;且压缩后的损坏历史被 memory.save() 持久化 → 此后每轮都 400,只能清空对话 SlidingWindowCompressor 的 preserveStart 纯按条数硬切 while (preserveStart > systemEnd && history[preserveStart] is LlmMessage.ToolResult) preserveStart--
P0-3 截断器反向膨胀:设置 Max Tool Output Length = 200(合法范围 200..100_000)时,head 1200 + tail 600 大于 maxChars → 250 字符输出变成 ≈2×原文,省略计数为负 [... -1550 chars omitted ...] ToolOutputTruncator 未收敛 head/tail safeHead = minOf(headChars, (maxChars*2/3).coerceAtLeast(1))、safeTail = minOf(tailChars, (maxChars - safeHead).coerceAtLeast(0))、计数 coerceAtLeast(0)
P0-4(审计) 线程契约违规:BrowserEngine.respondFileChooser 在 Dispatchers.IO 读写 WebView ValueCallback,而 onShowFileChooser 主线程写 → Chromium 主线程契约违反(native crash 或回调丢失死锁) ToolRegistry flowOn(Dispatchers.IO) + 主线程写入 改 suspend fun + withContext(Dispatchers.Main)
P0-4(评审) 预拷贝孤儿协程:每次 new CoroutineScope(Dispatchers.IO),Job 无人持有;50MB 附件选后立即移除,拷贝仍跑完;重选同文件 → 两路并发交错写同一目录 — PreprocessResult(..., job: Job?) + cancel(uri) + 进行中条目 putIfAbsent 登记复用

3. P1:泄漏与 OOM 三连

问题 现象 修复
TerminalCore mutation 列表无界 每字符追加一条 ScreenMutation 且 drainMutations() 全仓库无消费者(cat 50MB ≈ 5000 万对象) 有界累加器 BoundedMutationList(capacity = 4096),超限折叠为 ScreenMutation.FULL
VtParser CSI 参数无上限 畸形流永不出现 final byte → 无限累积 appendCsiParam/appendCsiIntermediate,超 MAX_CSI_BUFFER_LENGTH = 4096 转 CSI_IGNORE
TerminalEventLogImpl append-only 每 session 永久泄漏 超 MAX_EVENTS_PER_SESSION = 10_000 批量裁剪最旧 1_000 条(均摊 O(1))

其他 P1:

问题 修复
三处单例内 CoroutineScope(Dispatchers.Main).launch(连点 N 次 → N 个无主协程抢 stateMutex) 统一 mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
LogViewerScreen 导出捕获 Activity context 改 context.applicationContext
resume/retry/crash 三路径覆盖 currentJob 未先 cancel() 补 cancel()
RingTerminalBuffer writePos.get().toInt() 越过 2³¹ 回绕为负 minOf(writePos.get(), capacityBytes.toLong()).toInt()(先 Long 域再收窄)
CyberNeonBallManager.startPulse() 每次 new ObjectAnimator(INFINITE) 不保存引用 持 pulseAnimator 引用并 cancel()
ApexAccessibilityService 心跳在 Main 线程每 30s 做 binder IPC,无异常处理器 移 Dispatchers.Default + CoroutineExceptionHandler + runCatching

4. P1 边界值:SQLite 静默丢数据

Important

MemoryGraphStoreImpl 用 Room 的 IN (:fingerprints) 一次性传整棵 UI 树的指纹。 复杂页面 >999 节点 时,Android 8–10 的 SQLITE_MAX_VARIABLE_NUMBER = 999 触发 SQLiteException: too many SQL variables —— 被 MemoryWriterActor 吞掉 → 该帧记忆全部静默丢失。

修复:chunked(500).flatMap { dao.getByFingerprints(it) }。

VT 侧同类边界:

输入 后果 修复
\e[2147483647B cursorRow + n 回绕为负 → ArrayIndexOutOfBounds 参数 coerceIn(0, rows/cols)、结果 coerceIn(0, rows-1)
\e[999999999S repeat(n) 无上限 → ANR clamp 到 rows
\e[38;5;2147483647m 灰度分支 (index-232)*10+8 Int 溢出 统一 coerceIn(0, 255)

5. P2:兼容性与"永远连不上 / 永久明文"

问题 机制 修复
本地模型永远连不上 内置 Ollama / LM Studio / vLLM 预设都是 http://localhost:*,但无 networkSecurityConfig,targetSdk ≥ 28 默认禁明文 → Cleartext HTTP traffic not permitted 新增 res/xml/network_security_config.xml,仅对 localhost / 127.0.0.1 / 10.0.2.2 / 10.0.3.2 放行明文,云端 https 不受影响
GitHub token 永久明文 allowBackup="true" → 云备份恢复后 Tink keyset 与硬件绑定 MasterKey 不匹配 → EncryptedSharedPreferences.create 抛 AEADBadTagException → catch 静默降级为明文 SP data_extraction_rules.xml(API 31+)+ legacy_backup_rules.xml(API ≤30)排除两份 prefs
非流式响应解析 网关返回 200+HTML → SerializationException 绕过 LlmException,ErrorClassifier 失效;jsonPrimitive.int 对 128.0 抛 NumberFormatException intOrNull + 归一化 LlmException.Http(-1, "unparseable response body: ${body.take(200)}")
Android 13(API 33)全新安装 POST_NOTIFICATIONS 默认拒绝 → 前台服务通知静默不可见 launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
Shizuku 版本不一致 app 硬编码 dev.rikka.shizuku:api:13.1.0 vs catalog 13.1.5,Gradle 静默取高 → 移除 privilege 依赖会无声降级(api/provider 版本不一致 → binder 协议不匹配) 统一 implementation(libs.shizuku.api) + libs.shizuku.provider
Shizuku 未安装兜底 startActivity(ACTION_VIEW) 无 ActivityNotFoundException 防护 补防护
同毫秒文件名覆盖 / 未过滤 / File(targetDir, "${System.currentTimeMillis()}_$fileName") 加 uri.hashCode() + substringAfterLast('/') + 兜底名

6. 已排查确认健壮(避免重复审计)

  • Flow 收集全部 collectAsStateWithLifecycle(0 处裸 collectAsState() —— 审计后修);
  • PendingIntent 仅 1 处且带 FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT;
  • API 34 前台服务 specialUse + FOREGROUND_SERVICE_SPECIAL_USE + PROPERTY_SPECIAL_USE_FGS_SUBTYPE 齐全;
  • java.time / java.nio.file 在 minSdk 26 可用,无需 desugaring;
  • RootfsDownloader / RootfsExtractor:三态续传 + fd.sync + tar 校验和 + 路径穿越防护已加固;
  • okhttp 4.12.0(无 CVE-2023-3635)/ Coil 2.6.0 / Compose BOM 2024.12.01 稳定;
  • 全项目纯 Compose(grep Fragment 零命中)→ Fragment-not-attached 崩溃面不存在。

7. 遗留风险(登记未修)

风险 建议
BrowserEngine.onRenderProcessGone 会销毁正被浮窗承载的 WebView 需引擎 ↔ 浮窗 onWebViewDestroyed 通知协议(独立 PR)
dependencyLocking { lockAllConfigurations() } 无 lockfile 锁形同虚设,建议 ./gradlew :app:dependencies --write-locks 并提交 app/gradle.lockfile
security-crypto 1.1.0-alpha06 非稳定版且静默明文兜底,建议 Keystore + AES-GCM
browser/BrowserAgentTools 用废弃 getExternalStoragePublicDirectory 迁移到 MediaStore / 应用目录
TerminalViewModel 终端屏幕流无可见性降级 建议 LifecycleResumeEffect
ApexAccessibilityService.instance 静态引用 TOCTOU 建议 activeInstance()(AtomicBoolean bound 标志)

8. 必现崩溃的复现命令(P0-1)

adb install -r app-debug.apk
adb shell am start -n com.apex.agent/.MainActivity
adb shell am start-foreground-service -n com.apex.agent/.service.ApexCoreService
# 驱动到 WAITING_HUMAN 状态
adb logcat -d | grep -E "SecurityException|Requires VIBRATE"

# 修复验证
adb shell dumpsys package com.apex.agent | grep VIBRATE   # 期望 granted=true

9. 诚实声明规范

本项目反复出现的纪律(跨 T81/T82/MIGRATION/本审计一致): 本地验证的写本地验证,无法验证的明确标 NOT VERIFIED,绝不写假 PASS。

  • 进程死亡用确定性模拟 TaskRuntime.simulateCrash(),不声称真机 E2E;
  • androidTest 27 用例是 compile-only,真机从未执行(如实声明);
  • 审计报告里"CI 从不编译该模块测试"导致死测试套件掩埋 2 个 bug,也如实记录。

10. 相关页面

footer

🏠 返回首页 · 📚 文档索引 · ❓ FAQ · 🔧 故障排查 · 🗺️ 路线图 · 🐛 提 Issue

Android Guru Agent · v1.4.4 · Kotlin 2.0.21 · Compose · PRoot · Room

Clone this wiki locally