docs/current/06-出口与协议/堆有界隔离决议.md
GitHub ↗
当前有效

设计决议:堆内存改为 Bump 分配 + 有界隔离(Bounded Quarantine)

2490 字·约 7 分钟 阅读 2026-10-10 17:07

决议日期:2026-09-11(同日修订:v2,采纳 churn/leak 区分修正) 状态:已拍板 · 已实施(2026-09-11 落地于 Rust 冻结区,验收清单见 §6;MoonBit 侧同构落点 = vitro/engine/memory——MemoryMap bump + 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):

1MB 线性内存布局与堆有界隔离

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. 已知差异与代价(诚实记录)

  1. 隔离窗口外的 UAF 可能漏检:free(p) 与错误使用之间若隔离区已整体轮换(其间 churn 超过 256KB),*p 的访问落在已复用块上,仅表现为"读到别人的值"而非 UAF 诊断。与永久隔离的真实取舍,与 ASAN 行为一致,写入 C语言子集规范.md 已知差异:"Vitro 堆采用有界隔离(256KB)以强化 UAF 检测,隔离窗口内地址不复用(同 ASAN quarantine)";
  2. 外部碎片教学话题弱化——碎片仅在驱逐复用路径出现(first-fit 切分),Phase 14 碎片可视化 UI 改造为三色堆图,记录于 CHANGELOG;
  3. 防线 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)、VM free_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 不回退);3c differential_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 + 有界隔离替换其手段:不帮学生收拾,但把每一次没收拾的后果变成可见的教学信号——同时不惩罚正确收拾的学生。