目标: 通过工程规范 + 静态分析 + 代码审查 + 自动化测试,系统性防止内存泄漏、资源泄漏、UAF(Use-After-Free)和悬空指针问题。 适用范围(2026-10-05 删区后单轨):
moonbit/(唯一实现区;内存安全机制落点 =vitro/engine/memory〔check_access单入口 /FreedLogs有序结构 / bump + 有界隔离堆〕+host受检访存,S6 收官)。原native/Rust 冻结对照区已随 S9 工序④整体退役删除(tagrust-oracle-freeze)——文中「Rust」语言层规范与 Rust 代码示例均为删区前历史口径,保留为内存安全设计的方法学档案;线性内存 / 堆有界隔离 / 受检访存等语义章节仍为 MoonBit 侧现役语义。前端(Flutter/Dart)已于 2026-09-11 切割移交社区,本仓库不再包含 Dart 代码。最后核对日期:2026-10-05(删区对齐:适用范围改单轨、§三与 §5.2 标历史口径、压力测试与可观测节的等价防线改现役承接。前一沿革 2026-09-27:适用范围扩为双区并补 MoonBit 侧落点)
一、核心原则(不可违背)
原则 1:谁分配,谁释放(RAII)
- Rust:所有权系统自动管理内存;
Box<T>/Vec<T>/String在作用域结束时自动释放;禁止裸指针算术 - 前端(已迁出):原 Dart 侧通过 FRB 自动管理 Native 资源、
finalizer/dispose()释放大型资源;这些约定随前端切割移交社区(见 §2)
原则 2:事件订阅必须对称释放(前端契约 · 已随切割移交社区)
- 原规范要求 Riverpod provider 在
dispose()中取消订阅、AnimationController/ScrollController/TextEditingController必须dispose()、避免在StatefulWidget中创建长期存活的无名监听器。 - 这些是前端框架契约,随
CideFlutter/于 2026-09-11 移出本仓库,现属社区前端职责;后端无法也无需校验。历史实现见标签before-frontend-split。
原则 3:跨语言边界必须拷贝,不得传引用
- Rust C API:返回字符串禁止返回内部
String的裸指针;必须拷贝到调用者提供的缓冲区,或明确文档化生命周期("仅在下次编译前有效") - wasm 边界:跨 wasm 线性内存边界同样一律拷贝——
wasm-bindgen导出的是线性内存中的视图,JS 侧不得长期持有指向 VM 线性内存的裸视图(引擎 reset/重新分配后即失效);复杂结构走 JSON 字符串(见AGENTS.md架构纪律) - 宿主(C/JS/headless 客户端):禁止在宿主侧长期持有引擎内部指针;会话销毁后所有派生指针失效
原则 4:异步任务必须可取消
- Rust:
tokio::select!或自定义CancellationToken模式(当前 VM 为同步单步,无异步风险) - 前端(已迁出):原 Dart 侧
Future/Stream的cancel()/dispose()约定随切割移交社区
原则 5:并发释放必须线程安全
- Rust:利用所有权和
Mutex/RwLock保证并发安全;Drop实现避免阻塞操作 - 前端(已迁出):原"UI 操作必须在主线程、
dispose()中禁止异步阻塞"约定随切割移交社区
二、前端(Flutter/Dart)契约 · 已随切割移交社区
2026-09-11 前端切割:
CideFlutter/、FRB 桥接(native/src/api/、frb_generated)与全部 Dart 代码已从本仓库移除。本节原有的四条前端规范——StatefulWidget 生命周期契约(Controller/Listener 释放)、应用生命周期契约(Desktop 退出时不自动 dispose)、VM Session 释放模板、异步任务取消模板、CustomPainter 资源缓存——不再适用于本仓库,作为历史资产保留在标签before-frontend-split。移交给社区的契约要点(供社区前端自查):
契约 前端需保证 会话释放 每创建一个引擎会话(C ABI vitro_session_create/ serve 会话)必须成对销毁;应用退出时集中清理资源释放 控制器、监听器、动画对象在组件销毁时对称释放,禁止长期存活的无名监听器 异步取消 运行中被替换/取消的任务必须真正停止,且不得继续持有会话:引擎侧可用手段为 vitro_set_max_steps(步数上限)/ 会话销毁 / serve 的session.reset(当前没有vitro_stop一类的异步中断 API,如实记录)绘制资源 每帧回调中禁止重复创建绘制对象( Paint/Path/TextSpan等)后端只保证:会话 API 具备确定性销毁语义,且停止请求可被引擎接收;前端内存问题不由本仓库的测试防线覆盖。
三、Rust 后端规范(历史口径——实现已随 2026-10-05 删区退役;§四起的线性内存与堆隔离语义章节为 MoonBit 侧现役语义)
3.1 C API 设计规范
| 规则 | 示例 |
|---|---|
| 返回指针必须声明生命周期 | /// 仅在下次 compile 前有效 |
| 所有输出字符串参数必须提供缓冲区+长度 | char* buf, int buf_size |
| 输入数组必须提供长度+上限校验 | if (count > MAX_COUNT) return -1; |
| 版本号+校验和(序列化) | kSessionMagic = "VITROSV01" |
// ❌ 错误:返回悬空指针
#[no_mangle]
pub extern "C" fn vitro_get_errors(s: *mut Session) -> *const c_char {
let session = unsafe { &*s };
session.errors.as_ptr() // 重新分配后指针失效
}
// ✅ 正确:拷贝到调用者缓冲区
#[no_mangle]
pub extern "C" fn vitro_get_errors_buf(
s: *mut Session, buf: *mut c_char, max_len: i32
) -> i32 {
if s.is_null() || buf.is_null() || max_len <= 0 { return -1; }
let session = unsafe { &*s };
let c_str = CString::new(session.errors.clone()).unwrap_or_default();
let bytes = c_str.as_bytes_with_nul();
let copy_len = std::cmp::min(bytes.len(), max_len as usize - 1);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
copy_len as i32
}
3.2 VM 内存安全
// 所有 Host Function 内存访问必须通过封装接口
impl VitroVM {
fn load_i32(&self, addr: u32) -> Result<i32, Trap> {
let end = addr.checked_add(4)
.filter(|&end| end <= self.memory.len() as u32 && addr >= k_null_trap_size)
.ok_or_else(|| Trap::BoundsError(addr))?;
Ok(read_i32_le(&self.memory[addr as usize..end as usize]))
}
}
// 禁止裸指针算术
// ❌ memory[p1] = val; // p1 为负时缓冲区下溢
// ✅ self.store_i32(addr, val)?;
3.3 reset() 必须清理全部运行时状态
impl VitroVM {
fn reset(&mut self) {
self.code.clear();
self.stack.clear();
self.call_stack.clear();
self.func_table.clear(); // 不要忘记!
self.func_names.clear(); // 不要忘记!
self.host_funcs.clear();
self.symbols.clear();
self.vis_event_lines.clear(); // 不要忘记!
self.vis_event_queue.clear(); // 不要忘记!
self.breakpoints.clear(); // 不要忘记!
self.snapshot_vars.clear();
self.memory.fill(0);
self.step_count = 0;
self.heap_offset = k_heap_start;
// ... 其他状态归零
}
}
3.4 VM 线性内存是唯一的 1MB 堆:堆分配必须有界隔离
VM 的堆不是宿主 allocator,而是1MB 线性内存中的一段(k_heap_start 起);宿主侧只保留 region 表(记账),不保留用户数据指针。由此产生两条硬约束:
内存布局与有界隔离图(由 go run ./scripts/gen_svg 生成,对账本节与堆有界隔离决议):
- 堆上限即线性内存上限——用户程序
malloc到 1MB 墙时触发教学 trap("你的程序分配超过内存上限"),不允许穿透到宿主堆; - 释放必须进有界隔离区——
free不立即归还地址,而是标记后进入 FIFO 隔离区;隔离预算为堆上限的 1/4 = 256KB,超预算时按 FIFO 驱逐最老块复用(first-fit)。
| 场景 | 期望行为 | 定性 |
|---|---|---|
| churn(分配-释放循环) | 隔离区稳态在预算内,驱逐复用,无限可跑 | 合法程序,不误伤 |
| leak(只分配不释放) | leak 块不进隔离区,bump 推进直到撞 1MB 墙 | 教学信号(泄漏报告 + 耗尽诊断) |
| UAF | 检测窗口 = 最近 256KB 的 free 历史 | 教学检测 |
| Double-Free | 隔离窗口内地址不复用,双 free 必检出 | 教学检测 |
- 设计决议全文:堆有界隔离决议.md(2026-09-11 拍板并实施;含验收清单 §6;MoonBit 侧同构落点 =
vitro/engine/memory的MemoryMapbump + 隔离堆 + first-fit 驱逐) - 会话可配置:隔离预算可按会话调参(Rust 冻结区 capi
vitro_set_quarantine_budget/vitro_get_quarantine_budget,默认 256KB;MoonBit 侧会话配置随 S7 session 片落形) - 诚实记录(已知差异):隔离窗口外的 UAF 可能漏检——若
free(p)与错误使用之间 churn 已超过 256KB,*p会落在已复用块上,只表现为"读到别人的值"而非 UAF 诊断(与 ASAN quarantine 行为一致) - 协议侧映射:指针快照的
Freed状态判定依赖该隔离区(见docs/spec/STEP_PAYLOAD_SCHEMA_V0_1.md§3.1 说明) - 记账侧风险:region 表本身是宿主内存。极端 leak + 长步数场景下 region 记录数会增长(每
malloc至少数个 VM 步,默认 100k 步上限下约数万条记录、数 MB 宿主内存),由步数保险丝set_max_steps与 region 表封顶共同约束
四、代码审查 Checklist
4.1 PR 审查时必须逐项确认
前端(Flutter/Dart)自查项已随切割移除,移交社区(见 §2)。
Rust 后端:
- 所有权检查: 是否存在裸指针解引用未包裹
unsafe?unsafe块是否最小化? - C API 生命周期: 返回的裸指针是否有明确的生命周期文档注释
- Reset/clear: 新添加的容器字段是否在
reset()/clear()中处理 - 溢出检查:
addr + size、new_offset等是否使用checked_add - 跨语言边界: C API / wasm 导出是否返回内部指针/引用?
- 堆语义: 新增的分配/释放路径是否统一走隔离区出口(
allocate_raw/release_to_quarantine),而非绕过记账直接改指针? - 线性内存上限: 新路径是否仍受 1MB 墙约束(不得引入宿主堆旁路)?
4.2 高风险变更额外审查
以下变更类型必须双人审查:
- 新增 C API 函数(含 wasm 导出)
- 修改
VitroVM::reset()或内存管理逻辑 - 修改堆分配/释放语义(bump 指针、隔离区、region 表)
- 新增
unsafe块或裸指针操作
五、静态分析与工具链
5.1 Dart/Flutter 静态分析(已随切割移出)
原 analysis_options.yaml(strict-casts / strict-raw-types / missing_required_param / missing_return / unused_import / avoid_print 等规则)随 CideFlutter/ 于 2026-09-11 移出本仓库,当前仓库不存在该配置文件;这些规则属社区前端职责(历史资产,已迁出)。
5.2 Rust 分析器(Clippy——已随删区退役)
# 历史口径(Rust 区删区后无对象;MoonBit 侧等价 = moon 工具链检查 + surface/pkg_deps 静态闸)
cd native && cargo clippy --all-targets --all-features -- -D warnings
关键规则:
| 规则 | 说明 |
|---|---|
clippy::unwrap_used |
禁止在库代码中使用 .unwrap() |
clippy::expect_used |
限制 .expect() 的使用 |
clippy::missing_safety_doc |
unsafe 函数必须有安全文档 |
clippy::cast_lossless |
避免有损类型转换 |
5.3 预提交检查脚本(历史注记——已随删区删除)
scripts/check-memory-safety.ps1(扫描 Rust 源码内存安全反模式)为"存在但未接线"的工具(2026-09-11 核实全仓零消费),已随 2026-10-05 删区批物理删除;历史形态见 git 历史。MoonBit 侧等价静态闸 = moonbit_surface / pkg_deps / source_hygiene / mbti_sync 族。
六、测试策略
说明:§6.1 与 §6.3 的示例是 C# 前端时代(Phase 3 之前)的遗留代码,不是当前技术栈(现为 Rust + 三出口),仅作"Dispose 路径应被测试"的思路参照,保留为历史资产;§6.2 的 C ABI 压力测试思路仍然有效。
6.1 单元测试:Dispose 路径覆盖(历史示例 · C# 前端时代)
[Test]
public void CompilerService_Dispose_IsIdempotent()
{
var svc = new CompilerService();
svc.Dispose();
svc.Dispose(); // 不应抛异常
Assert.IsTrue(svc.IsDisposed);
}
[Test]
public void MainViewModel_Dispose_CancelsPendingFlash()
{
var vm = new MainViewModel();
vm.RunCodeAsync(); // 启动某些异步任务
vm.Dispose(); // 应取消所有任务
// 验证无异常
}
6.2 Stress Test:重复编译-运行-Destroy(思路有效)
TEST(MemoryStress, CompileRunDestroy_1000Times) {
for (int i = 0; i < 1000; ++i) {
auto* s = vitro_session_create();
vitro_compile(s, "int main() { return 0; }");
vitro_run(s);
vitro_session_destroy(s);
}
// 进程 RSS 不应持续增长
}
这是当前仍然适用的压力测试思路。原等价防线(
native/tests/host_contract_tests.rs+ fuzz_stress_test)已随删区退役——现役承接 =scripts/host_contract_map(冻结名册对 MoonBit 锚三态对账,条数见该脚本rust_contract_names.json)+moon test内 fuzz 不变量自检 + clang_direct 直拍。wasm 出口的等价压力测试尚未建立(本仓库不含 wasm 侧用例驱动),如实记为缺口。
6.3 监控指标(历史示例 · C# 前端时代)
以下示例随 C# 前端时代结束而失效,不作为当前实现依据,仅保留其"应观测峰值内存与释放状态"的意图:
// 历史示例(当前技术栈无此代码)
Debug.WriteLine($"[VITRO_MEM] MainViewModel disposed. Compiler disposed: {_compiler?.IsDisposed}");
Debug.WriteLine($"[VITRO_MEM] CompilerService disposed. Session: {_session}");
Debug.WriteLine($"[VITRO_MEM] Process exit. PeakWorkingSet: {Process.GetCurrentProcess().PeakWorkingSet64 / 1024}KB");
当前对应的可观测手段:
cmd/serve的 JSON-lines 会话帧(MoonBit 侧现役)、vitro step的执行摘要等。⚠️ 尚无内存/堆统计的协议导出——原 capi 计划项vitro_get_heap_stats_json已随 U2 拍板(2026-09-19)裁不做(capi 出口随删区整体裁撤);MoonBit 侧堆统计出口随 S8 教学智能片裁定(memory.regions三段式地图已可作观测通道)。
七、相关文档索引
| 文档 | 说明 |
|---|---|
| 架构设计.md | 总体架构设计 |
| 构建指南.md | 构建指南 |
| 堆有界隔离决议.md | 堆内存 Bump 分配 + 有界隔离决议(256KB 隔离预算) |
| STEP_PAYLOAD_SCHEMA_V0_1.md | 三出口载荷 schema(指针四状态等) |
| 后端定位与白箱计划.md | 前端切割与三出口路线 |
| ARCHIVE_INCIDENT_2026_04_26_MEMORY_LEAK.md | 历史泄漏事件记录与修复详情 |
本文档由 Kimi Code CLI 维护。每次发现新的内存安全问题时,必须同步更新本文档的"代码审查 Checklist"和"相关文档索引"。
2026-09-11 修订:完成前端切割后的现状对齐(适用范围改纯后端仓、前端契约压缩移交、补 wasm 线性内存与堆有界隔离、注明预提交脚本零消费引用)。