From 9e8ee96491e0293a356762fa33219a5b1ac7662b Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 19:02:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=9F=E6=88=90=E5=99=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=9B=92=E6=B7=B1=E6=8B=B7=E8=B4=9D=E6=B6=88=E9=99=A4?= =?UTF-8?q?=20sweep/promote=20=E5=85=B1=E4=BA=AB=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=9D=83=20UAF=EF=BC=8Cfull=5Freset=20=E8=A1=A5=E9=87=8A?= =?UTF-8?q?=E6=94=BE=20session=20=E5=A0=86=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GeneratorState 状态盒此前由 epoch 原对象独占释放,promote 与 sweep 克隆 浅拷贝 native_data 共享同一盒,旧对象 drop 即悬垂。新增 clone 深拷贝 (参照 async_generator),promote/sweep 两处接入,恢复执行读新盒; full_reset 在 session arena 重置前遍历 session 对象释放堆数据, 配合深拷贝形成单所有权。补 sweep/promote/full_reset 三回归与 生成器捕获闭包跨 GC 回归。 --- crates/oxide_vm/src/generator.rs | 18 ++++ crates/oxide_vm/src/session_arena.rs | 4 +- crates/oxide_vm/src/session_gc.rs | 2 + crates/oxide_vm/src/vm_support.rs | 92 ++++++++++++++++++++- crates/oxide_vm/tests/generator_gc_tests.rs | 36 ++++++++ 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 crates/oxide_vm/tests/generator_gc_tests.rs diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index 1c0f0530..a5ae2497 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -872,6 +872,24 @@ pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(J state.suspended.rewrite_values(rewrite); } +/// 深拷贝状态盒到新对象(promote / sweep 用):新对象持独立 Box,源盒可安全释放。 +pub(crate) fn clone_generator_native_with_rewrite( + old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue, +) { + let Some(state) = generator_state_mut(old) else { + return; + }; + let cloned = GeneratorState { + phase: state.phase, + callee: rewrite(state.callee), + args: state.args.iter().copied().map(&mut rewrite).collect(), + this_value: state.this_value, + result: rewrite(state.result), + suspended: state.suspended.clone_with_rewrite(rewrite), + }; + new.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8); +} + /// 释放生成器状态盒(对象被 GC 回收时),返回释放字节数。 pub(crate) fn drop_generator_native(obj: &JsObject) -> u64 { if !obj.is_generator_obj() { diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index c1b592dd..fc713d89 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -66,8 +66,8 @@ impl Vm { self.promote_value_if_epoch_object(value, forwarding) }); } else if src_ref.is_generator_obj() { - // 生成器状态盒为堆分配、promote 后与源共享同一盒:就地改写其 JsValue。 - crate::generator::rewrite_generator_native(dst_ref, |value| { + // 生成器状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。 + crate::generator::clone_generator_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); } else if src_ref.is_promise_obj() { diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 548fe95d..57c55be2 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -434,6 +434,8 @@ impl SessionGc { crate::async_func::clone_async_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_async_generator_obj() { crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_generator_obj() { + crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value); } forwarding.insert(old_ptr, new_ptr); freed_bytes += Self::drop_session_object_heap_data(old_ptr); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index ed84e0b5..12c7696c 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -302,8 +302,18 @@ impl Vm { self.free_epoch_object_heap_data(); self.epoch.reset(); self.gc_state.epoch_object_ptrs.clear(); + // 先释放 session 对象堆数据(属性向量 + 各原生盒),再重置 arena: + // 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。 + let mut freed = 0u64; + for ptr in self.gc_state.session_object_ptrs.drain(..) { + freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, true); + } + if freed > 0 { + self.gc_state.session_gc.total_bytes_freed = + self.gc_state.session_gc.total_bytes_freed.saturating_add(freed); + self.gc_state.session_gc.last_collection_bytes_freed = freed; + } self.gc_state.session_epoch.reset(); - self.gc_state.session_object_ptrs.clear(); self.gc_state.session_bytes_allocated = 0; self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold; self.gc_state.session_gc = crate::session_gc::SessionGc::new(); @@ -660,6 +670,86 @@ mod tests { assert!(!vm.session.is_dirty_since_snapshot()); } + fn vm_with_low_threshold() -> Vm { + let mut cfg = KernelConfig::minimal(); + cfg.set_session_gc_threshold(1); + Vm::with_kernel_core(KernelCore::new(cfg)) + } + + /// 直接恢复生成器一步:等价 `it.next()`,但避免跨 run(sub_modules 重建会使 + /// 挂起帧失效)——回归聚焦 GC 搬移后的状态盒有效性。 + fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue { + match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) { + Ok(crate::generator::GeneratorStep::Suspended { value }) => value, + Ok(crate::generator::GeneratorStep::Completed { value }) => value, + Ok(other) => panic!("unexpected step: {:?}", match other { + crate::generator::GeneratorStep::Thrown { value } => format!("Thrown({value})"), + crate::generator::GeneratorStep::SuspendedRaw { value } => format!("SuspendedRaw({value})"), + _ => String::new(), + }), + Err(e) => panic!("resume failed: {e}"), + } + } + + #[test] + fn generator_survives_object_sweep_and_resumes() { + let mut vm = vm_with_low_threshold(); + let _ = run_source( + &mut vm, + "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0", + ); + + // 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena, + // 状态盒深拷贝为新 Box(reset 会清空模块表使恢复不可行,走收集入口等价验证)。 + vm.maybe_collect_session_gc(); + assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集"); + + // 从 global 取 sweep 重写后的生成器(sub_modules/immutables 未重建,可恢复)。 + let it = global_prop(&vm, "it"); + assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复第二次 yield"); + } + + #[test] + fn generator_captured_upvalue_survives_sweep() { + let mut vm = vm_with_low_threshold(); + let _ = run_source( + &mut vm, + "var x = 0; function* g(){ x++; yield x; x++; yield x; } globalThis.it = g(); globalThis.it.next(); 0", + ); + + vm.maybe_collect_session_gc(); + assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集"); + + // 挂起帧 cell_stack 与闭包 upvalues 中的 cell 独立堆分配(地址稳定), + // 恢复后继续读写捕获变量。 + let it = global_prop(&vm, "it"); + assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复捕获变量读写"); + } + + #[test] + fn generator_promoted_clone_owns_independent_state_box() { + let mut vm = Vm::new(); + // `var it = g(); it.next(); it`:it 为 epoch 生成器对象(未逃逸不 promote)。 + let it = run_source(&mut vm, "function* g(){ yield 1; yield 2; } var it = g(); it.next(); it"); + assert!(it.is_object()); + let epoch_ptr = it.as_js_object_ptr(); + let epoch_box = unsafe { (*epoch_ptr).native_data() }; + + // 手动 promote:克隆应深拷贝状态盒(新 Box),与源盒互不共享。 + let promoted = vm.promote_object(epoch_ptr); + assert!(!std::ptr::eq(promoted, epoch_ptr)); + let promoted_box = unsafe { (*promoted).native_data() }; + assert!(!std::ptr::eq(epoch_box, promoted_box), "promote 应深拷贝生成器状态盒"); + + // 模拟 full_reset 的 epoch 侧释放:源对象与其状态盒随 epoch 回收, + // 并从追踪表移除登记(克隆的后续回收仍由 VM 统一处理)。 + let _ = crate::session_gc::SessionGc::drop_object_heap_data(epoch_ptr, false); + vm.gc_state.epoch_object_ptrs.retain(|&p| !std::ptr::eq(p, epoch_ptr)); + + // 克隆直接恢复执行:读新盒中的挂起状态,不得悬垂。 + assert_eq!(resume_one_step(&mut vm, JsValue::from_js_object(promoted)), JsValue::int(2)); + } + #[test] fn full_reset_clean_keeps_session_objects() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/tests/generator_gc_tests.rs b/crates/oxide_vm/tests/generator_gc_tests.rs new file mode 100644 index 00000000..b642bc83 --- /dev/null +++ b/crates/oxide_vm/tests/generator_gc_tests.rs @@ -0,0 +1,36 @@ +//! 生成器 × session GC 回归:promote 后 full_reset 释放 epoch 原对象与 session 克隆 +//! 的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。 +//! sweep 变体与闭包捕获变体见 `vm_support.rs` 内部测试(需 crate 内 `resume_generator` +//! 在模块表未重建时恢复执行,integration 层无法触达)。 + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn compile(source: &str) -> oxide_bytecode::module::CompiledModule { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + Compiler::new().compile(&program).expect("compile") +} + +/// promote + full_reset 变体:global 根持有生成器克隆,full_reset 释放 epoch 原对象 +/// 与 session 克隆的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。 +#[test] +fn generator_promoted_to_global_survives_full_reset_cleanly() { + let mut vm = Vm::new(); + vm.run(&compile("function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0")) + .expect("run1"); + assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session"); + + // global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问 + // (修复前 epoch 原对象释放共享状态盒 → 读已释放内存)。 + vm.full_reset(); + assert_eq!(vm.session_object_count(), 0, "full_reset 应清空 session 对象"); + + let result = vm.run(&compile("typeof it")).expect("run2"); + let text = vm.lookup_str(result).expect("typeof 应返回字符串").to_string(); + assert_eq!(text, "undefined"); + // 新会话可正常创建并推进生成器。 + let ok = vm.run(&compile("function* h(){ yield 9; } var it2 = h(); it2.next().value")).expect("run3"); + assert_eq!(format!("{ok}"), "9"); +}