diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 38a88373..92d0f418 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -165,8 +165,7 @@ impl Vm { if current_cells[cell_idx].is_null() { // 占位 Cell 保持未初始化(TDZ)直到 MAKE_CELL 置位; // 若绑定为 var 则其初始化 MAKE_CELL 在函数序言先于任何读取执行。 - let cell = self.gc_state.session_epoch.alloc(Cell::new(JsValue::undefined(), false)); - current_cells[cell_idx] = cell as *mut Cell; + current_cells[cell_idx] = self.gc_state.alloc_cell(JsValue::undefined(), false); } current_cells[cell_idx] }; @@ -190,8 +189,7 @@ impl Vm { } if current[cell_idx].is_null() { // 无占位 cell:新建。 - let cell = self.gc_state.session_epoch.alloc(Cell::new(value, true)); - current[cell_idx] = cell as *mut Cell; + current[cell_idx] = self.gc_state.alloc_cell(value, true); } else { // 更新占位 cell(CREATE_CLOSURE 已建),使闭包 upvalue 指向的 // cell 值跟随初始化;赋值即解除 TDZ。 @@ -217,8 +215,7 @@ impl Vm { while current.len() <= cell_idx { current.push(std::ptr::null_mut()); } - let cell = self.gc_state.session_epoch.alloc(Cell::new(value, true)); - current[cell_idx] = cell as *mut Cell; + current[cell_idx] = self.gc_state.alloc_cell(value, true); Ok(()) } @@ -231,8 +228,7 @@ impl Vm { } if current[cell_idx].is_null() { let val = self.regs[a]; - let cell = self.gc_state.session_epoch.alloc(Cell::new(val, true)); - current[cell_idx] = cell as *mut Cell; + current[cell_idx] = self.gc_state.alloc_cell(val, true); } let c = unsafe { &*current[cell_idx] }; if !c.is_initialized() { @@ -311,9 +307,8 @@ impl Vm { } else { self.regs[rd] }; - let cell = self.gc_state.session_epoch.alloc(Cell::new(val, true)); - upvals[uv_idx] = cell as *mut Cell; - self.regs[rd] = cell.value; + upvals[uv_idx] = self.gc_state.alloc_cell(val, true); + self.regs[rd] = unsafe { (*upvals[uv_idx]).value }; return Ok(()); } } @@ -344,8 +339,7 @@ impl Vm { } vm_debug!("STORE_UPVALUE len={} wrote existing", upvals.len()); } else { - let cell = self.gc_state.session_epoch.alloc(Cell::new(src_val, true)); - upvals[uv_idx] = cell as *mut Cell; + upvals[uv_idx] = self.gc_state.alloc_cell(src_val, true); } } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 72687424..548fe95d 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -741,8 +741,11 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje #[cfg(test)] mod tests { + use oxide_compiler::compiler::Compiler; use oxide_kernel::kernel::{KernelConfig, KernelCore}; + use oxide_parser::Allocator; use oxide_types::object::JsObject; + use oxide_types::value::JsValue; use super::*; use crate::vm::{CallFrame, FrameContinuation}; @@ -1358,6 +1361,89 @@ mod tests { Vm::with_kernel_core(core) } + // ── upvalue cell 独立堆分配:跨对象 sweep 存活 ───────────────────────── + + 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") + } + + fn global_prop_opt(vm: &Vm, name: &str) -> Option { + let global = vm.session.global_object(); + let si = vm.kernel_core().perm_interner().intern(name).0; + vm.resolve_property(global, si) + } + + /// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换), + /// 修复前 cell 分配于旧 arena、随 Bump drop 悬垂;修复后独立堆分配、 + /// 地址稳定,sweep 只重写 cell.value 中的对象引用,值跨搬移保留。 + #[test] + fn closure_cell_survives_object_sweep() { + let mut vm = vm_with_threshold(1); + vm.run(&compile("var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; 0")) + .expect("run1"); + assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell"); + + // reset 触发对象 sweep:存活对象搬到新 arena,旧 arena 释放。 + vm.reset(); + assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集"); + + // 跨搬移后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。 + let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上"); + let obj = unsafe { &*inc_val.as_js_object_ptr() }; + let cells = obj.upvalues_slice(); + assert_eq!(cells.len(), 1, "inc 应捕获 counter 一个 cell"); + let cell = unsafe { &*cells[0] }; + assert_eq!(cell.value, JsValue::int(0), "sweep 后 cell 值应保留为 counter 初值"); + assert!(cell.is_initialized(), "sweep 后 cell 初始化位应保留"); + } + + /// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell + /// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址, + /// cell 指针仍稳定(修复前 cell 悬垂、值被复用覆盖)。 + #[test] + fn private_brand_cell_survives_object_sweep() { + let mut vm = vm_with_threshold(1); + vm.run(&compile( + "class C { #x = 0; set(v){ this.#x = v; } get(){ return this.#x; } } \ + globalThis.c = new C(); globalThis.c.set(4); 0", + )) + .expect("run1"); + + vm.reset(); + assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集"); + + // 实例 c 的方法 get(类原型上)捕获 @@class_brand cell:值须仍为对象。 + let c_val = global_prop_opt(&vm, "c").expect("c 应挂在 global 上"); + let c_obj = unsafe { &*c_val.as_js_object_ptr() }; + let proto_ptr = c_obj.proto().as_js_object_ptr(); + let get_val = vm + .resolve_property(unsafe { &*proto_ptr }, vm.kernel_core().perm_interner().intern("get").0) + .expect("类原型应有 get 方法"); + let get_obj = unsafe { &*get_val.as_js_object_ptr() }; + let cells = get_obj.upvalues_slice(); + assert!(!cells.is_empty(), "get 应捕获 @@class_brand cell"); + let brand = unsafe { &*cells[0] }.value; + assert!(brand.is_object(), "sweep 后 brand cell 值应仍为 brand 对象"); + } + + /// full_reset 统一释放全部 cell box 且可重新分配:追踪表清空(无 double-free), + /// 重置后新闭包正常建立新 cell。 + #[test] + fn cells_freed_by_full_reset_and_reallocatable() { + let mut vm = Vm::new(); + vm.run(&compile("var x = 1; function f() { return x; } globalThis.f = f; f()")) + .expect("run1"); + assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty()); + + vm.full_reset(); + assert!(vm.gc_state.session_cell_ptrs.borrow().is_empty(), "full_reset 应释放全部 cell"); + + let result = vm.run(&compile("var y = 2; function g() { return y; } g()")).expect("run2"); + assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取"); + } + #[test] fn allocation_does_not_trigger_gc_before_safe_point() { let mut vm = vm_with_threshold(1); diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 994fc1a0..e0cad9bb 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -15,7 +15,7 @@ use rustc_hash::FxBuildHasher; use crate::session_gc::SessionGc; use crate::vm::ForInIter; -use oxide_types::object::{JsObject, JsString}; +use oxide_types::object::{Cell as UpvalueCell, JsObject, JsString}; use oxide_types::value::JsValue; /// session arena 与 GC 簿记。 @@ -33,6 +33,10 @@ pub(crate) struct GcState { /// `convert_immutables` 也能登记新 box。与字符串不同,BigInt 不参与 /// mark/sweep 回收(值量少),只在 full_reset 统一释放。 pub(crate) session_bigint_ptrs: RefCell>, + /// upvalue cell(`Box`)追踪表。`RefCell` 使 `&self` 的分配入口也能 + /// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写 + /// `cell.value` 中的对象引用),只在 full_reset 统一释放。 + pub(crate) session_cell_ptrs: RefCell>, pub(crate) session_bytes_allocated: usize, /// 执行期字符串 GC 的触发水位:本次收集后的存活字节 + 阈值增量。 /// 仅当账目超过水位才在指令边界触发回收——活串超阈值时不会每指令重复 @@ -45,6 +49,29 @@ impl GcState { pub(crate) fn track_epoch_object(&mut self, ptr: *mut JsObject) { self.epoch_object_ptrs.push(ptr); } + + /// 分配一个 upvalue cell:独立堆分配并返回裸指针,脱离 session arena 生命周期。 + /// + /// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 sweep + /// 搬移不触碰 cell 结构体(地址稳定),只重写 `cell.value` 中的对象引用。 + /// `&self` 使调用方可与 `cell_stack` 等其它字段的借用并存(分字段借用)。 + pub(crate) fn alloc_cell(&self, value: JsValue, initialized: bool) -> *mut UpvalueCell { + let ptr = Box::into_raw(Box::new(UpvalueCell::new(value, initialized))); + self.session_cell_ptrs.borrow_mut().push(ptr); + ptr + } + + /// 释放全部 session 堆 upvalue cell box。仅在完全隔离重置(`full_reset`)时调用, + /// 此时没有存活的 cell_stack / 函数对象 upvalues 会引用它们。 + pub(crate) fn free_cells(&mut self) { + for ptr in self.session_cell_ptrs.borrow_mut().drain(..) { + // SAFETY: 每个指针来自 alloc_cell 的 Box::into_raw(Box::new(Cell)), + // 且只在这里恰好释放一次。 + unsafe { + drop(Box::from_raw(ptr)); + } + } + } } /// Symbol 的 intern 状态。 diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 26a3b008..bc162134 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -92,6 +92,7 @@ impl Vm { session_object_ptrs: Vec::new(), session_string_ptrs: Vec::new(), session_bigint_ptrs: std::cell::RefCell::new(Vec::new()), + session_cell_ptrs: std::cell::RefCell::new(Vec::new()), session_bytes_allocated: 0, string_gc_watermark: gc_threshold, forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), @@ -193,6 +194,7 @@ impl Vm { session_object_ptrs: Vec::new(), session_string_ptrs: Vec::new(), session_bigint_ptrs: std::cell::RefCell::new(Vec::new()), + session_cell_ptrs: std::cell::RefCell::new(Vec::new()), session_bytes_allocated: 0, string_gc_watermark: gc_threshold, forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), @@ -300,6 +302,7 @@ impl Vm { self.gc_state.session_gc = crate::session_gc::SessionGc::new(); self.free_session_string_heap_data(); self.free_session_bigint_heap_data(); + self.gc_state.free_cells(); self.symbols.reset(); self.root_reg_limit = 0; self.active_reg_limit = 0; diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 9c03e718..f48ce01c 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -227,3 +227,20 @@ fn full_reset_clears_session_after_runtime_gc() { let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string(); assert_eq!(text, "ok!"); } + +/// full_reset 统一释放执行期分配过的 upvalue cell 无 double-free:重置后引擎 +/// 可继续运行并重建新闭包(cell 独立堆分配,随 full_reset 恰好释放一次)。 +#[test] +fn closure_cells_freed_by_full_reset_without_double_free() { + let mut vm = vm_with_threshold(1); + let first = compile("var x = 1; function f() { return x; } globalThis.f = f; f()"); + let result = vm.run(&first).expect("run1"); + assert_eq!(format!("{}", result), "1"); + + vm.full_reset(); + + let second = compile("'ok' + '!'"); + let result = vm.run(&second).expect("run2"); + let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string(); + assert_eq!(text, "ok!"); +}