diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 77475f27..a5a77f8c 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -102,6 +102,8 @@ impl Vm { dst } + /// 克隆改写与 reset 边界晋升共用的单引用改写:epoch 对象克隆晋升进 + /// session(forwarding 去重共享与环);其余值原样保留。 pub(crate) fn promote_value_if_epoch_object( &mut self, value: JsValue, forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, ) -> JsValue { @@ -109,21 +111,100 @@ impl Vm { return value; } let ptr = value.as_js_object_ptr(); - if ptr.is_null() || !unsafe { &*ptr }.is_epoch() { + if ptr.is_null() { return value; } - JsValue::from_js_object(self.promote_object_inner(ptr, forwarding)) + // SAFETY: 执行核心产出的对象值,指针在 session 生命周期内有效。 + if unsafe { &*ptr }.is_epoch() { + return JsValue::from_js_object(self.promote_object_inner(ptr, forwarding)); + } + value } + /// epoch 边界前的 session 原地晋升:遍历全部 session 对象,把仍指向 epoch + /// 对象的引用克隆进 session(JS 边 + native 状态盒),消除 epoch 重置后的 + /// 悬垂指针。 + /// + /// 覆盖两类绕过写屏障的引用来源:函数对象直接 session 分配后捕获的 + /// `captured_this`/`home_object`/upvalue cell/属性值(SET_HOME_OBJECT 与 + /// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值。 + /// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/ + /// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。 + pub(crate) fn promote_session_epoch_refs(&mut self) { + let objects = std::mem::take(&mut self.gc_state.session_object_ptrs); + if objects.is_empty() { + return; + } + let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); + for &ptr in &objects { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 session_epoch.alloc,arena 存活期内有效。 + unsafe { + let obj = &mut *ptr; + obj.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, &mut forwarding)); + if obj.is_map() { + map::rewrite_map_native(obj, |value| self.promote_value_if_epoch_object(value, &mut forwarding)); + } else if obj.is_set() { + set::rewrite_set_native(obj, |value| self.promote_value_if_epoch_object(value, &mut forwarding)); + } else if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() { + disposable_stack::rewrite_dispose_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_typed_array_obj() { + typed_array::rewrite_typed_array_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_data_view_obj() { + data_view::rewrite_data_view_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_generator_obj() { + crate::generator::rewrite_generator_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_promise_obj() { + crate::promise::rewrite_promise_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_async_obj() { + crate::async_func::rewrite_async_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } else if obj.is_async_generator_obj() { + crate::async_generator::rewrite_async_generator_native(obj, |value| { + self.promote_value_if_epoch_object(value, &mut forwarding) + }); + } + } + } + forwarding.clear(); + self.gc_state.forwarding = forwarding; + // 晋升过程新克隆的对象已推入 gc_state 侧的表,拼回旧表保持原序在前。 + self.gc_state.session_object_ptrs.extend(objects); + } + + /// 逃逸写屏障:写向全局/session 对象的对象值不指向 epoch,否则 epoch + /// 重置后悬垂。session 函数目标豁免晋升——克隆会令写入侧与字节码持有的 + /// 原始对象分裂(类构造器在原型上续建方法会落到非克隆体);函数持有的 + /// epoch 子引用由 `promote_session_epoch_refs` 在 epoch 边界统一修复。 pub(crate) fn promote_if_needed_for_write_ptr(&mut self, target_ptr: *mut JsObject, value: JsValue) -> JsValue { if !value.is_object() || !self.is_session_escape_root_ptr(target_ptr) { return value; } let value_ptr = value.as_js_object_ptr(); - if value_ptr.is_null() || !unsafe { &*value_ptr }.is_epoch() { + if value_ptr.is_null() { return value; } - JsValue::from_js_object(self.promote_object(value_ptr)) + // SAFETY: 执行核心产出的对象值,指针在 session 生命周期内有效。 + if unsafe { &*target_ptr }.is_function() { + return value; + } + if unsafe { &*value_ptr }.is_epoch() { + return JsValue::from_js_object(self.promote_object(value_ptr)); + } + value } } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 5cc049eb..f9858f0d 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -408,6 +408,9 @@ impl Vm { pub fn reset(&mut self) { self.clear_execution_state(); self.maybe_collect_session_gc(); + // session 对象可持有 epoch 子引用(函数对象捕获、原生盒直插):epoch + // 重置前把 epoch 子引用原地克隆晋升进 session,避免悬垂指针。 + self.promote_session_epoch_refs(); self.bytecode = Arc::default(); self.immutables_cache.clear(); self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); @@ -542,7 +545,12 @@ impl Vm { obj.set_arrow(true); obj.set_captured_this(self.regs[254]); } - let obj_ptr = self.alloc_object(obj); + // 函数对象直接 session 分配:寿命从调用级延至 session 级(session GC + // mark/sweep 回收)。若按 epoch 分配,写入全局等逃逸根时 promote 屏障会 + // 深克隆进 session,全局属性与局部槽指针分裂、严格相等恒 false。 + obj.set_session_epoch(true); + let obj_ptr = self.gc_state.session_epoch.alloc(obj) as *mut JsObject; + self.gc_state.session_object_ptrs.push(obj_ptr); let func_val = JsValue::object(obj_ptr as *mut u8); if !is_arrow { @@ -554,8 +562,13 @@ impl Vm { } else { JsValue::from_js_object(self.session.builtin_world().object_proto.as_ptr() as *mut JsObject) }; - let prototype_obj = self.epoch.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_of_proto)); - self.gc_state.track_epoch_object(prototype_obj); + // prototype 子对象与函数本体同走 session 分配:`f.prototype === + // globalThis.f.prototype` 要求两侧同一对象,epoch 分配会在逃逸写时 + // 被递归克隆出第二份。 + let mut prototype = JsObject::new_empty(EMPTY_SHAPE_ID, proto_of_proto); + prototype.set_session_epoch(true); + let prototype_obj = self.gc_state.session_epoch.alloc(prototype) as *mut JsObject; + self.gc_state.session_object_ptrs.push(prototype_obj); let prototype_val = JsValue::from_js_object(prototype_obj); if !is_generator { diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 5b835c25..fe102a2c 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -222,6 +222,27 @@ fn full_reset_clears_session_after_runtime_gc() { assert_eq!(text, "ok!"); } +/// 低阈值下 reset 触发完整对象 sweep(移动式搬移 + 重标):写全局的函数对象 +/// 不被当死对象释放,run2 仍为可引用的函数值(跨 run 调用受独立的 +/// sub_module_index 缺口限制,不在此断言)。 +#[test] +fn reset_sweep_preserves_global_function_object() { + let mut vm = vm_with_threshold(1024); + let first = compile( + "function make(){ var o = {v: 9}; globalThis.fn = function(){ return o.v; }; } make(); \ + for (var i = 0; i < 2000; i++) { var t = 'p' + i + 'q' + i; } \ + typeof globalThis.fn === 'function'", + ); + let result = vm.run(&first).expect("run1"); + assert!(result.is_bool() && result.as_bool()); + assert!(vm.session_gc_stats().total_collections > 0, "低阈值应触发执行期收集"); + vm.reset(); + + let second = compile("typeof globalThis.fn === 'function'"); + let result = vm.run(&second).expect("run2"); + assert!(result.is_bool() && result.as_bool()); +} + /// full_reset 统一释放执行期分配过的 upvalue cell 无 double-free:重置后引擎 /// 可继续运行并重建新闭包(cell 独立堆分配,随 full_reset 恰好释放一次)。 #[test] diff --git a/crates/oxide_vm/tests/global_var_binding_tests.rs b/crates/oxide_vm/tests/global_var_binding_tests.rs index 718a99bf..89f9a13d 100644 --- a/crates/oxide_vm/tests/global_var_binding_tests.rs +++ b/crates/oxide_vm/tests/global_var_binding_tests.rs @@ -22,6 +22,21 @@ fn eval_string(source: &str) -> String { vm.lookup_str(result).unwrap_or_default() } +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") +} + +/// 两阶段执行:phase1 → `reset()`(轻量重置,epoch 清空、session 保留)→ phase2。 +fn eval_two_phases(phase1: &str, phase2: &str) -> bool { + let mut vm = Vm::new(); + vm.run(&compile(phase1)).expect("run1"); + vm.reset(); + let result = vm.run(&compile(phase2)).expect("run2"); + result.is_bool() && result.as_bool() +} + #[test] fn top_level_var_lands_on_global_this() { eval_truthy("var x = 5; globalThis.x === 5"); @@ -138,7 +153,91 @@ fn function_callable_before_its_declaration() { #[test] fn var_no_init_does_not_clobber_function_binding() { eval_truthy("function f(){ return 1; } var f; typeof f === 'function' && f() === 1"); - // 不比较 globalThis.f === f 同一性:属性读路径存在预存 codegen 缺陷, - // 行为契约是声明后属性仍为原函数对象(可调用、返回值正确)。 - eval_truthy("function f(){ return 1; } var f; typeof globalThis.f === 'function' && globalThis.f() === 1"); + // 无初始化 var 声明不触碰函数绑定:声明后全局属性仍为原函数对象, + // 同一性与行为同时保持。 + eval_truthy("function f(){ return 1; } var f; globalThis.f === f && globalThis.f() === 1"); +} + +/// 函数对象写全局不分裂:同一函数值经逃逸写屏障后严格相等保持 true, +/// 覆盖函数声明、var 初始化、赋值、箭头/生成器/async 函数各形态。 +#[test] +fn function_written_to_global_keeps_identity() { + eval_truthy("function f(){ return 1; } globalThis.f = f; globalThis.f === f"); + eval_truthy("var g = function(){}; globalThis.g = g; globalThis.g === g"); + eval_truthy("var h; h = function(){}; globalThis.h = h; globalThis.h === h"); + eval_truthy("var a = () => 42; globalThis.a = a; globalThis.a === a"); + eval_truthy("var ge = function*(){ yield 1; }; globalThis.ge = ge; globalThis.ge === ge"); + eval_truthy("var af = async function(){}; globalThis.af = af; globalThis.af === af"); +} + +/// prototype 子对象与函数本体同一:逃逸写后 `f.prototype === globalThis.f.prototype`, +/// 且经全局别名继续修改 prototype 对局部别名可见。 +#[test] +fn function_prototype_identity_survives_global_write() { + eval_truthy("function f(){} globalThis.f = f; globalThis.f.prototype === f.prototype"); + eval_truthy("function f(){} globalThis.f = f; globalThis.f.prototype.z = 5; globalThis.f.prototype.z === 5"); + // 手工替换 prototype:接收者经函数别名重读,函数目标写入不克隆。 + eval_truthy( + "var f = function(){}; f.prototype = {}; f.prototype.m = function(){ return 3; }; f.prototype.m() === 3", + ); + eval_truthy("var f = function(){}; globalThis.f = f; f.prototype.m = function(){ return 3; }; globalThis.f.prototype.m() === 3"); +} + +/// 类构造器形状:原型为运行时新建对象,构造器/方法/prototype 链接在逃逸写 +/// 后不分裂——实例方法可调用、constructor 指回类、派生类 super 派发正常。 +#[test] +fn class_constructor_shape_after_global_write() { + eval_truthy("var cc = class { m(){ return 3; } }; cc.prototype.constructor === cc"); + eval_truthy("var cc = class { m(){ return 3; } }; new cc().m() === 3"); + eval_truthy("var cc = class { m(){ return 3; } }; typeof cc.prototype.m === 'function'"); + eval_truthy( + "var Base = class { c(){ return 11; } }; \ + var D = class extends Base { d(){ return super.c(); } }; \ + new D().d() === 11", + ); +} + +/// 嵌套闭包写全局:内层函数经外层调用写 globalThis,两侧同一函数对象。 +#[test] +fn nested_closure_written_to_global_keeps_identity() { + eval_truthy("var n = function(){ var m = function(){ return 7; }; globalThis.m = m; return m; }; var m1 = n(); globalThis.m === m1"); +} + +/// 同一函数写两个全局槽:两槽指向同一对象。 +#[test] +fn same_function_written_to_two_globals_is_identical() { + eval_truthy("function f(){} globalThis.f = f; globalThis.f2 = f; globalThis.f2 === globalThis.f"); +} + +/// 跨 run(reset)契约:函数对象直落 session,其属性持有的 epoch 对象 +/// (prototype 槽)在 epoch 清空前被晋升,run2 经全局别名可读其属性。 +/// 不跨 run 调用存活函数:sub_module_index 指向上一执行期的子模块表, +/// 跨执行调用是独立缺口(基线即失败,非本次修复范围)。 +#[test] +fn global_function_survives_reset_with_promoted_property() { + assert!(eval_two_phases( + "var f = function(){}; globalThis.f = f; f.prototype = {}; f.prototype.q = 7; 0", + "typeof globalThis.f === 'function' && globalThis.f.prototype.q === 7", + )); +} + +/// 类构造器跨 run:prototype 子对象(运行时 epoch 新建)经 reset 边界晋升, +/// run2 经全局别名读到方法属性。 +#[test] +fn class_prototype_link_survives_reset() { + assert!(eval_two_phases( + "var cc = class { m(){ return 3; } }; globalThis.cc = cc; 0", + "typeof globalThis.cc === 'function' && typeof globalThis.cc.prototype.m === 'function'", + )); +} + +/// Map 原生盒按键值直插不经写屏障:epoch 值跨 reset 由边界晋升克隆进 +/// session。run2 先分配新 epoch 对象覆写旧内存再读盒内值——未晋升则读到 +/// 覆写后的垃圾。 +#[test] +fn map_box_value_survives_reset() { + assert!(eval_two_phases( + "var key = {k: 1}; globalThis.key = key; var val = {v: 2}; globalThis.m = new Map(); globalThis.m.set(globalThis.key, val); 0", + "var fill; for (var i = 0; i < 200; i++) { let t = {junk: i, pad: 'x'.repeat(32)}; fill = t; } globalThis.m.get(globalThis.key).v === 2", + )); }