From ad0405fe512bf2407e0051c91022315fa932137f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 11 Sep 2026 19:43:55 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20full=5Freset/run=20=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E6=8D=A2=E6=96=B0=20Bump=20=E6=9B=BF=E4=BB=A3=E5=8E=9F?= =?UTF-8?q?=E5=9C=B0=20reset=E2=80=94=E2=80=94=E6=97=A7=20epoch/session=20?= =?UTF-8?q?arena=20=E5=85=A8=E9=87=8F=E5=BD=92=E8=BF=98=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=86=E9=85=8D=E5=99=A8=E3=80=81=E5=AE=B9=E9=87=8F=E4=B8=8D?= =?UTF-8?q?=E8=B7=A8=20reset=20=E4=BF=9D=E7=95=99=EF=BC=8C=E9=85=8D=20aren?= =?UTF-8?q?a=20=E4=BF=9D=E7=95=99=E9=94=9A=E6=A0=A1=E5=87=86=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_cli/src/bench/leak_detect.rs | 62 +++++++++++++++++++++++ crates/oxide_cli/src/bench/mod.rs | 1 + crates/oxide_types/src/mem.rs | 17 +++++-- crates/oxide_vm/src/vm.rs | 10 ++++ crates/oxide_vm/src/vm_support.rs | 27 ++++++++-- crates/oxide_vm/tests/mem_tests.rs | 11 ++++ 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/crates/oxide_cli/src/bench/leak_detect.rs b/crates/oxide_cli/src/bench/leak_detect.rs index 26c8d8ad..8f870607 100644 --- a/crates/oxide_cli/src/bench/leak_detect.rs +++ b/crates/oxide_cli/src/bench/leak_detect.rs @@ -374,6 +374,68 @@ pub fn run_mem_closure_dead_leak(kernel: &Arc) -> ExitCode { report_series("closure_dead_leak", "iter", &series) } +/// 校准用例:重源单次 run 后 `full_reset()`,读双 arena 保留锚 +/// (`arena_retained_bytes`,reset 边界换新 Bump 后恰 0),再 50 轮轻源 +/// (每轮 `full_reset()`)采 VmRSS,度量 full_reset 归还路径的跨轮内存 +/// 增量(斜率 + 窗口总增量),期望走平。 +/// +/// # 注意事项 +/// 不走 VM 池:drop clean 路径与池内归还走同一 `full_reset`,池锁/condvar +/// 噪音与本测面无关。重源瞬时对象全 epoch 分配、不逃逸 global;锚 == 0 为 +/// 确定性主门槛(in-engine,免分配器噪音),RSS 为次锚(进程级,仅判 +/// 峰后无新增量——归还 chunk 可能滞留分配器自由链不回 OS,不要求回落 +/// 峰前)。 +pub fn run_mem_pool_high_water(kernel: &Arc) -> ExitCode { + const LIGHT_ROUNDS: usize = 50; + + let heavy_js = "var t = 0; for (var i = 0; i < 200000; i++) { var o = { s: 'abcdefghij' + i, a: [i, i + 1, i + 2] }; t += o.s.length + o.a.length; } t"; + let light_js = "1 + 1"; + let compile_one = |js: &str| { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, js).expect("parse failed"); + let hash = compiled_module_hash(&program); + kernel + .code_forge() + .get_or_insert_with(hash, || Compiler::new().compile(&program)) + .expect("compile failed") + }; + let heavy = compile_one(heavy_js); + let light = compile_one(light_js); + + let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); + if let Err(e) = vm.run(&heavy) { + eprintln!("[pool_high_water] heavy run failed: {e}"); + return ExitCode::FAILURE; + } + vm.full_reset(); + let anchor = vm.arena_retained_bytes(); + eprintln!("[pool_high_water] anchor after full_reset: {anchor} bytes (expect 0)"); + if anchor != 0 { + return ExitCode::FAILURE; + } + + let mut sampler = LeakSampler::new(20); + let mut series: Vec<(usize, f64)> = Vec::new(); + for i in 0..LIGHT_ROUNDS { + if let Err(e) = vm.run(&light) { + eprintln!("[pool_high_water] round {i} run failed: {e}"); + return ExitCode::FAILURE; + } + vm.full_reset(); + if let Some(kb) = read_vmrss_kb() { + let v = kb as f64; + series.push((i, v)); + if let Some(verdict) = sampler.add_sample(i, v) { + eprintln!( + "[LEAK] pool_high_water rss_kb: slope={:.6} R²={:.4} over {} samples", + verdict.slope, verdict.r2, 20 + ); + } + } + } + report_series("pool_high_water", "round", &series) +} + /// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总 /// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。 fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode { diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs index 2bad556b..80f2bf1e 100644 --- a/crates/oxide_cli/src/bench/mod.rs +++ b/crates/oxide_cli/src/bench/mod.rs @@ -41,6 +41,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc, pool: Arc leak_detect::run_mem_vm_creation_leak(&kernel), Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_leak(&kernel), Some("closure_dead_leak") => leak_detect::run_mem_closure_dead_leak(&kernel), + Some("pool_high_water") => leak_detect::run_mem_pool_high_water(&kernel), _ => leak_detect::run_leak_detect(&config, &kernel, &pool), }, _ => { diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index 459c8a18..a6c22a46 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -1,8 +1,9 @@ //! 内存管理抽象:跨 epoch 持久存储与每次调用(agent call)内的 arena 分配。 //! //! `P` 是 `Arc` 的透明包装,持有者跨 `Epoch::reset()` 存活; -//! `Epoch` 则是对 `bumpalo::Bump` 的封装,用于每次调用内的高频分配与 -//! O(1) 整体回收,并通过 epoch ID 辅助悬挂指针检测。 +//! `Epoch` 则是对 `bumpalo::Bump` 的封装,用于每次调用内的高频分配, +//! `reset()` 换新 arena 整体回收(旧 arena 全量归还系统分配器,地址空间 +//! 不复用),并通过 epoch ID 辅助悬挂指针检测。 use std::fmt; use std::ops::Deref; @@ -67,7 +68,8 @@ impl fmt::Display for P { } /// 包装 `bumpalo::Bump` 并携带 epoch ID 计数器用于悬挂指针检测。 -/// 所有 Agent 调用级对象分配于此;`reset()` 在每次调用结束时 O(1) 清空。 +/// 所有 Agent 调用级对象分配于此;`reset()` 在每次调用结束时换新 arena, +/// 旧 arena 内存全量归还系统分配器。 pub struct Epoch { bump: bumpalo::Bump, epoch_id: u64, @@ -128,10 +130,15 @@ impl Epoch { } } - /// O(1) 批量释放。此前所有分配立即失效。 + /// 换新空 `Bump` 整体回收:旧 arena 的全部 chunk 归还系统分配器, + /// 地址空间不复用,此前所有分配立即失效。 /// 递增 epoch ID 以使过期指针失效(debug_assert 守卫)。 + /// + /// # 注意事项 + /// 调用点须保证该边界无存活旧 arena 指针(登记表已清、执行态已空)。 + /// 换 Bump 使漏晋升的陈旧指针由静默别名新对象转为显式 UAF,属护栏加强。 pub fn reset(&mut self) { - self.bump.reset(); + self.bump = bumpalo::Bump::new(); self.epoch_id += 1; } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index fbde4bea..42392508 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -992,6 +992,16 @@ impl Vm { self.gc_state.session_bytes_peak } + /// 双 arena(epoch + session 对象)当前 chunk 已分配字节之和。 + /// + /// # 注意事项 + /// reset 边界读 = 换新 Bump 后空 arena 恰 0(空 chunk 哨兵计 0); + /// run 中途读 = 当前 chunk 用量(非全 arena 口径——run 计量走 + /// [`Self::run_alloc_bytes`],勿据此断言)。 + pub fn arena_retained_bytes(&self) -> usize { + self.epoch.bump().allocated_bytes() + self.gc_state.session_epoch.allocated_bytes() + } + /// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆 /// 账目(session 串 + session 对象及其属性向量 + GC 后补回的 BigInt)。 /// 单次 run 内单调不减(执行期对象不回收、 diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index d8d7778e..2a6380cb 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -343,7 +343,9 @@ impl Vm { self.teardown_session_heap_data(); self.epoch.reset(); self.gc_state.epoch_object_ptrs.clear(); - self.gc_state.session_epoch.reset(); + // 换新 Bump:旧 session arena 全量归还系统分配器(与 sweep 路径同构), + // 容量不跨 full_reset 保留。 + self.gc_state.session_epoch = bumpalo::Bump::new(); self.gc_state.session_bytes_allocated = 0; self.gc_state.session_bytes_peak = 0; self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold; @@ -1201,6 +1203,23 @@ mod tests { assert!(unsafe { *session_ptr } == 123); } + /// run 边界换新 Bump 后双 arena 保留锚恰 0:重源 run 冲高水位, + /// full_reset 后 epoch/session 两 arena 均空(容量不跨 reset 保留)。 + #[test] + fn full_reset_zeroes_arena_retained() { + let mut vm = Vm::new(); + run_source( + &mut vm, + "var t = 0; for (var i = 0; i < 20000; i++) { var o = { s: 'ab' + i, a: [i] }; t += o.s.length + o.a.length; } t", + ); + assert!(vm.epoch.bump().allocated_bytes() > 0, "重源 run 应冲高 epoch arena 水位"); + + vm.full_reset(); + + assert_eq!(vm.epoch.bump().allocated_bytes(), 0); + assert_eq!(vm.gc_state.session_epoch.allocated_bytes(), 0); + } + #[test] fn immutables_cache_filled_once_per_module() { let mut vm = Vm::new(); @@ -1318,12 +1337,12 @@ mod tests { } #[test] - fn session_epoch_reset_is_only_in_full_reset_state_clear() { + fn session_epoch_replacement_is_only_in_full_reset_state_clear() { let src = include_str!("vm_support.rs"); let production = src.split("#[cfg(test)]").next().expect("production source"); - assert_eq!(production.matches("self.gc_state.session_epoch.reset()").count(), 1); + assert_eq!(production.matches("self.gc_state.session_epoch = bumpalo::Bump::new()").count(), 1); assert!(production.contains("fn clear_full_reset_state(&mut self)")); - assert!(production.contains("self.gc_state.session_epoch.reset();")); + assert!(production.contains("self.gc_state.session_epoch = bumpalo::Bump::new();")); } #[test] diff --git a/crates/oxide_vm/tests/mem_tests.rs b/crates/oxide_vm/tests/mem_tests.rs index 3d26b95b..f651d76a 100644 --- a/crates/oxide_vm/tests/mem_tests.rs +++ b/crates/oxide_vm/tests/mem_tests.rs @@ -27,6 +27,17 @@ fn epoch_reset_increments_id() { assert_eq!(epoch.current_id(), 2); } +/// 换新 Bump 后旧地址空间不复用:reset 边界双 arena 保留锚恰 0 +/// (空 chunk 哨兵计 0),容量不跨 reset 保留。 +#[test] +fn epoch_reset_frees_arena() { + let mut epoch = Epoch::new(); + let _ptr = epoch.alloc(vec![0u8; 1 << 20]); + assert!(epoch.bump().allocated_bytes() > 0); + epoch.reset(); + assert_eq!(epoch.bump().allocated_bytes(), 0); +} + #[test] fn persistent_new_and_deref() { let p = P::new(42i32);