决议日期:2026-09-11(同日修订:v2,采纳 churn/leak 区分修正) 状态:已拍板 · 已实施(2026-09-11 落地于 Rust 冻结区,验收清单见 §6;MoonBit 侧同构落点 =
vitro/engine/memory——MemoryMapbump + FIFO 隔离 + first-fit 驱逐 +check_access单入口,S6 已收官随 0.6.0 发布) 归属:主计划 后端定位与白箱计划.md 的引擎层决策,Phase 1 内可实施(与切割无依赖)v2 修订说明:v1 为"永久隔离"(free 后地址永不复用),评审指出其误伤合法 churn 程序(
while(记录) { p=malloc(100); 用; free(p); }在真实 C 可无限跑,永久隔离下约 1 万轮撞 1MB 墙)。v2 改为 ASAN 原版的有界隔离:隔离区超预算时驱逐最老块复用——churn 与 leak 由此正确分离。
1. 决策
Vitro 堆采用 bump 分配 + 有界隔离:
malloc:顶指针 O(1) 推进;隔离区超预算时先驱逐(FIFO 最老已释放块归还复用,first-fit);free:块进入 FIFO 隔离区(标记 + 数据保留),不立即归还;- 隔离预算:堆上限的 1/4 = 256KB(可调,写进会话配置);
- 进程(VM 实例)结束随 1MB 线性内存整体回收。
业界先例:ASAN quarantine 原版机制(有界隔离区 + FIFO 驱逐复用)。隔离窗口保证 UAF/Double-Free 检测覆盖,驱逐复用保证合法 churn 不误伤。
内存布局与有界隔离图(由 go run ./scripts/gen_svg 生成,对账本决议与内存安全规范 §3.4):
2. 动机(churn / leak 正确分离)
| 场景 | 行为 | 定性 |
|---|---|---|
| churn(正确分配-释放循环) | 隔离区稳态在预算内,最老块驱逐复用,无限可跑 | 合法程序,不误伤 |
| leak(只分配不释放) | leak 块不进隔离区(未被 free),bump 持续推进 → 撞 1MB 墙 | 教学信号(泄漏报告 + 耗尽诊断) |
| UAF | 检测窗口 = 最近 256KB 的 free 历史;学生 UAF 几乎总紧跟 free 之后,覆盖率极高 | 教学检测 |
| Double-Free | 隔离窗口内地址不复用,双 free 必检出 | 教学检测 |
| 时间旅行 / 判分 | 隔离窗口内内存单调;驱逐后变化仍在快照机制覆盖内 | CoW 与确定性受益 |
| 性能 | free_list 维护频率 = 驱逐频率(churn 稳态下低频),malloc 主路径仍 O(1) | 收敛 V-P1-11 一半问题 |
3. 语义变更
| 操作 | 旧 | 新 |
|---|---|---|
malloc |
free_list 查找 + 合并 | 顶指针推进;隔离区超预算 → FIFO 驱逐最老块归还 first-fit |
free |
标记 + 立即归还 free_list | 标记 + 进 FIFO 隔离区;freed_logs / 泄漏判定 / E3027·E3061 诊断全部保留 |
realloc |
原地扩展或搬移 | 恒为新块拷贝(与 glibc 常见路径一致) |
| 内存可视化 | 分配块/空闲块(含外部碎片图) | 已分配/隔离中(最近释放)/ 可复用三色——churn 与 leak 在图上一目了然 |
教学语义红利:churn 循环超过隔离预算后,free 后再 malloc 真实得到复用地址——"free 的作用"演示真机可做(v1 永久隔离下需要单独 demo workaround,v2 不再需要)。
4. 已知差异与代价(诚实记录)
- 隔离窗口外的 UAF 可能漏检:
free(p)与错误使用之间若隔离区已整体轮换(其间 churn 超过 256KB),*p的访问落在已复用块上,仅表现为"读到别人的值"而非 UAF 诊断。与永久隔离的真实取舍,与 ASAN 行为一致,写入C语言子集规范.md已知差异:"Vitro 堆采用有界隔离(256KB)以强化 UAF 检测,隔离窗口内地址不复用(同 ASAN quarantine)"; - 外部碎片教学话题弱化——碎片仅在驱逐复用路径出现(first-fit 切分),Phase 14 碎片可视化 UI 改造为三色堆图,记录于 CHANGELOG;
- 防线 3 语义对齐——host_contract_tests(3a)、differential_stress(3c)、fuzz E 的 free 语义断言按新语义重写(把"分配器复用行为"从契约中除名的正规流程,非粉饰)。
5. 边界推导
- churn 稳态:隔离区字节恒 ≤ 256KB(驱逐维持),bump 不推进——无限循环不撞墙;
- leak 路径:leak 块不进隔离区,bump 单调推进 → 1MB 耗尽 → 教学 trap("你的程序分配超过内存上限");步数保险丝(
set_max_steps)先触发时 region 表同步封顶(每 malloc 至少数个 VM 步,默认 100k 步 ≈ 数万条 region 记录,数 MB host 内存); - 混合场景(leak + churn 并存):churn 部分稳态复用,leak 部分推进——墙留给真正的泄漏。
6. 验收清单
- host_malloc / host_free / host_realloc 按新语义实现(隔离区 + FIFO 驱逐 + first-fit 归还),merge_free_list 移除或降级为驱逐路径内部实现;
→
MemoryState::allocate_raw(bump + 驱逐 + first-fit)/release_to_quarantine(free 唯一出口)/evict_quarantine(FIFO 驱逐,内部调用 merge);merge_free_list降级为驱逐路径内部实现。三条释放路径(host_free、realloc(p,0)、VMfree_memory)统一走隔离区出口;fopen的 FILE* 分配亦改走统一分配入口。 - churn 无限循环用例:超隔离预算的
malloc(100)/free循环 10 万次不撞墙、地址在驱逐后复用(防 v1 误伤回归); →crash_regression_tests.rs::test_heap_churn_beyond_quarantine_budget_no_wall(reuse=1+churn ok;反证:无复用则 10485 次即撞墙)。 - UAF / Double-Free / 泄漏报告 / 无效 free 诊断在隔离窗口语义下全部回归(
crash_regression_tests.rs补专项:free 后立即读必检出、隔离窗口内双 free 必检出、窗口外漏检案例标注为已知差异); → 新增test_heap_uaf_within_quarantine_window_detected(E3060)、test_heap_double_free_within_quarantine_window_detected(E3061);窗口外漏检记入C语言子集规范.md§2.9-1;泄漏报告与无效 free 诊断由全量防线回归。 - 防线 3a / 3c / fuzz E 断言重写并全绿;
→ 3a(
host_contract_tests.rs)新增 4 条隔离区契约(free 入隔离区不入 free_list / 驱逐后地址复用 / realloc 必搬移 / heap_offset 不回退);3cdifferential_stress与 fuzz A/E 的断言(基于freed_logs实时取址)与新语义天然兼容,全量回归通过。 -
C语言子集规范.md已知差异补录(§4-1);CHANGELOG 记录碎片可视化资产处置; → 新增 §2.9「堆分配模型:bump + 有界隔离」(含四项与 Clang 的差异);CHANGELOG 记录碎片统计语义收窄与三色堆图随 capi 第二批落地。 - §1「隔离预算可调,写进会话配置」的对外暴露(补齐完整性缺口,2026-09-11):
vitro_set_quarantine_budget/vitro_get_quarantine_budget(native/src/capi/first_batch.rs)——budget = 0关闭隔离(教学对照)、超大值裁剪到堆上限(1MB)、负值拒绝;vitro_cli serve经config.set/config.get暴露同一字段。 → 测试:capi_first_batch_tests::test_set_quarantine_budget_controls_address_reuse(默认 256KB 下 free 后不复用 → 预算 0 时立即复用)、test_quarantine_budget_is_clamped_to_heap_limit、test_quarantine_budget_setter_rejects_null_session; serve 侧由scripts/serve_smoke.py断言默认值 262144 与config.set生效。 - 三道墙用例(1MB 耗尽 / max_steps / region 表封顶推导验证)—— 2026-09-11 全部补齐:
→ 1MB 耗尽:
crash_regression_tests::test_heap_1mb_wall_returns_null_with_teaching_hint(NULL + 教学提示); → 步数保险丝:crash_regression_tests::test_second_wall_max_steps_fuse(并回显配置值(1000 步)); → region 表封顶:crash_regression_tests::test_third_wall_region_table_bounded_when_step_fuse_trips_first(leak 路径上步数保险丝先触发时,region 条数 ≤ 步数上限、heap_offset 未达 1MB)。 > 补用例时发现的严重缺陷(已修,2026-09-11):compile_pipeline::setup_vm里硬编码 >vm.set_max_steps(10_000_000),每次 run 都抹掉会话配置 —— 步数保险丝对全速运行的程序 > 从未生效(实测设 2000 步的程序跑到 16 万步、撞 1MB 堆墙才停)。旧用例只断言"消息含步数超限", > 1000 万步同样满足,故长期掩盖。同批修复:capi/serve 在会话无 VM 时静默丢弃配置、 >config()不回显上限。回归见native/tests/session_config_test.rs(4 用例)。 - Shadow 门禁全绿(含 K&R/LeetCode 内存密集用例)。 → C shadow 632 用例与 C++ shadow 100 用例均 0 非预期差异(2026-09-11 实测,见 CHANGELOG)。
7. 对"伪 GC"PR 的处置
拒绝真 GC 方向:GC 自动回收会掩盖学生最需要学的错误——本项目核心教学资产就是 free 语义的教学(泄漏报告、UAF/Double-Free 知识卡片),GC 等于把考点删了。采纳其动机(内存管理简化),以 bump + 有界隔离替换其手段:不帮学生收拾,但把每一次没收拾的后果变成可见的教学信号——同时不惩罚正确收拾的学生。