From 4285086b0edbe7273a535e20455571a07e1f4685 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 20 Sep 2026 06:30:54 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=86=99=E5=B1=8F=E9=9A=9C=E9=80=80?= =?UTF-8?q?=E7=9B=B4=E9=80=9A=E5=B9=B6=E6=8C=89=E8=B6=9F=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20global=20=E7=9A=84=20epoch=20=E5=AD=90=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_arena.rs | 73 +++--- crates/oxide_vm/src/vm_support/tests.rs | 14 +- .../tests/write_barrier_identity_tests.rs | 242 ++++++++++++++++++ 3 files changed, 286 insertions(+), 43 deletions(-) create mode 100644 crates/oxide_vm/tests/write_barrier_identity_tests.rs diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 1b925a6e..d0b55329 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -9,14 +9,9 @@ use crate::vm::Vm; use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array}; impl Vm { - pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool { - if target_ptr.is_null() { - return false; - } - let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; - std::ptr::eq(target_ptr, global_ptr) || unsafe { (&*target_ptr).is_session_epoch() } - } - + /// 单对象晋升测试入口:取/清共享转发表后调 `promote_object_inner`。 + /// 生产路径(边界修复/执行期晋升档/根晋升)各自持表管理,不经过此包装。 + #[cfg(test)] pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject { vm_debug!("promote_object: src={:p}", src); let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); @@ -135,9 +130,11 @@ impl Vm { /// 对象的引用克隆进 session(JS 边 + native 状态盒),消除 epoch 重置后的 /// 悬垂指针。 /// - /// 覆盖两类绕过写屏障的引用来源:函数对象直接 session 分配后捕获的 + /// 覆盖三类绕过写屏障的引用来源:函数对象直接 session 分配后捕获的 /// `captured_this`/`home_object`/upvalue cell/属性值(SET_HOME_OBJECT 与 - /// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值。 + /// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值; + /// global 对象的属性值(逃逸写直通后 epoch 值直落 global 槽,global 不入 + /// session 对象表,由 `rewrite_session_epoch_refs` 同趟改写)。 /// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/ /// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。 /// @@ -145,11 +142,9 @@ impl Vm { /// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入): /// 本方法原地改写 session 对象的引用字段,执行期调用将使 builtin 局部 /// 裸指针失效(与 `collect_session_gc` 同前提)。 + /// - session 对象表为空时 global 的 epoch 子引用仍须修复,不做空表早退。 pub 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); self.rewrite_session_epoch_refs(&objects, &mut forwarding); forwarding.clear(); @@ -163,6 +158,9 @@ impl Vm { /// epoch 子对象经转发表克隆进 session(递归去重共享与环),已是 session/ /// 非 epoch 的值原样保留。JS 边与 native 状态盒走同一改写闭包。 /// + /// global 对象不入 session 对象表(P 根,不在 arena 内):其 epoch 子引用 + /// 与列表同趟改写,无 native 盒只覆盖 JS 边。 + /// /// # 注意事项 /// - 调用方持有转发表期间不得让其它晋升路径改动 `gc_state.forwarding`; /// 晋升新克隆推入的对象表由调用方决定何时拼回。 @@ -218,6 +216,14 @@ impl Vm { } } } + // global 对象不入 session 对象表:其 epoch 子引用(逃逸写直通留存、 + // 函数目标豁免、native 盒直插)与列表同趟修复,免 epoch 释放后悬垂。 + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + // SAFETY: global 归本 session 所有,安全点内指针有效。 + unsafe { + let obj = &mut *global_ptr; + obj.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding)); + } } /// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session, @@ -257,25 +263,15 @@ impl Vm { self.gc_state.forwarding = forwarding; } - /// 逃逸写屏障:写向全局/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() { - return value; - } - // 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)); - } + /// 逃逸写屏障:写向全局/session 目标的对象值原样直通。 + /// + /// session 对象持有 epoch 子引用是 GC 设计的一等存活态:epoch 边界 reset + /// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环), + /// 执行期收集晋升档按同一转发表改写根与存活 session 对象子引用,两条 + /// 路径覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针 + /// 全程有效。此处若克隆,同一逻辑对象分裂为原件与克隆两份,经克隆的写 + /// 对方经原件的读不可见(builtin 帧指针与逃逸目标双引用并存时即暴露)。 + pub(crate) fn promote_if_needed_for_write_ptr(&mut self, _target_ptr: *mut JsObject, value: JsValue) -> JsValue { value } } @@ -407,20 +403,19 @@ mod tests { } #[test] - fn session_arena_barrier_promotes_global_root_write() { + fn session_arena_barrier_global_root_write_returns_original() { let mut vm = Vm::new(); let value = JsValue::from_js_object(plain_object(&mut vm)); let global_ptr = vm.session.global_object().as_ptr() as *mut JsObject; let promoted = vm.promote_if_needed_for_write_ptr(global_ptr, value); - assert!(promoted.is_object()); - assert!(!is_epoch_object(&vm, promoted)); - assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() }); + assert_eq!(promoted, value); + assert!(is_epoch_object(&vm, promoted), "epoch 子引用应直通留存,由边界/晋升档统一修复"); } #[test] - fn session_arena_barrier_promotes_already_session_target_write() { + fn session_arena_barrier_session_target_write_returns_original() { let mut vm = Vm::new(); let target_epoch = plain_object(&mut vm); let target = vm.promote_object(target_epoch); @@ -428,8 +423,8 @@ mod tests { let promoted = vm.promote_if_needed_for_write_ptr(target, value); - assert!(!is_epoch_object(&vm, promoted)); - assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() }); + assert_eq!(promoted, value); + assert!(is_epoch_object(&vm, promoted), "session 目标上的 epoch 值应直通留存"); } #[test] diff --git a/crates/oxide_vm/src/vm_support/tests.rs b/crates/oxide_vm/src/vm_support/tests.rs index 8358ec55..a4afad38 100644 --- a/crates/oxide_vm/src/vm_support/tests.rs +++ b/crates/oxide_vm/src/vm_support/tests.rs @@ -58,15 +58,21 @@ fn sweep_runner_forges_rejects_live_vm() { #[test] fn full_reset_with_session_objects_forces_global_rebuild() { let mut vm = Vm::new(); - // 池路径场景:`globalThis.Array = {}` 覆盖既有 global 槽(不递增 generation), - // 新值 `{}` 经 promote 进入 session——global 保留时该指针将悬垂。 + // 池路径场景:`globalThis.Array = {}` 覆盖既有 global 槽——写屏障直通令 + // 新值 `{}` 留存 epoch,global 持 epoch 子引用;global 保留时该指针将悬垂。 + // 覆盖写推进 global generation,快照对比发现脏 global,full_reset 必须重建。 let _ = run_source(&mut vm, "globalThis.Array = {}; 0"); - assert!(!vm.gc_state.session_object_ptrs.is_empty(), "覆盖写应触发 promote 进入 session"); + let written = global_prop(&vm, "Array"); + // SAFETY: 覆盖值是本 VM 自有的 epoch 对象,本 session 内指针有效。 + assert!( + unsafe { (&*written.as_js_object_ptr()).is_epoch() }, + "覆盖值应直通留存 epoch,不克隆进 session" + ); let old_global = vm.session.global_object.as_ptr(); vm.full_reset(); - // global 必须重建:旧 global 与其 session 对象随 epoch 释放,Array 恢复内置构造器。 + // global 必须重建:旧 global 与其 epoch 子引用随 epoch 释放,Array 恢复内置构造器。 assert!(!std::ptr::eq(old_global, vm.session.global_object.as_ptr())); assert!(std::ptr::eq( global_prop(&vm, "Array").as_js_object_ptr(), diff --git a/crates/oxide_vm/tests/write_barrier_identity_tests.rs b/crates/oxide_vm/tests/write_barrier_identity_tests.rs new file mode 100644 index 00000000..2a7b4448 --- /dev/null +++ b/crates/oxide_vm/tests/write_barrier_identity_tests.rs @@ -0,0 +1,242 @@ +//! 逃逸写屏障 identity 分裂判别钉(写屏障退直通后:原件与逃逸目标恒同一对象)。 +//! +//! 覆盖形态:IIFE 纯 JS 逃逸写、flat/flatMap/splice/for-of/嵌套元素窗内访问器 +//! 逃逸写、setter 深度、晋升压载下双引用一致性;对照钉钉住 native 盒双态一致 +//! 与跨 reset 边界克隆收敛。期望值与 node 行为一致。 +//! +//! 关键不变式:逃逸写(对象值写向全局/session 目标)之后,原件引用与逃逸目标 +//! 引用指向同一对象,经任一路径的读写互见;执行期晋升与边界修复不引入第二份。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +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") +} + +fn vm_default() -> Vm { + Vm::new() +} + +/// 低 session GC 阈值 VM:执行期两档收集在少量分配内即可触发。 +fn vm_with_threshold(bytes: usize) -> Vm { + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(bytes); + Vm::with_kernel_core(KernelCore::new(config)) +} + +/// 执行源码并取字符串完成值。 +fn eval_string(vm: &mut Vm, source: &str) -> String { + let result = vm.run(&Arc::new(compile(source))).expect("run"); + vm.lookup_str(result).expect("完成值应为字符串").to_string() +} + +/// P1 最基础形态:IIFE 内 `g = inner; g[1] = 99`(无 builtin 无 getter), +/// 逃逸写后原件、逃逸目标值与 identity 三读一致。 +#[test] +fn escape_write_iife_global_no_accessor() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + g = inner; \ + g[1] = 99; \ + return inner[1] + '|' + g[1] + '|' + (inner === g); \ + })()", + ); + assert_eq!(out, "99|99|true", "逃逸写后原件与逃逸目标应同一对象且值互见"); +} + +/// P2 flat 主形态:getter 内逃逸写后,flat 续读应见写入值而非原件旧值。 +#[test] +fn escape_write_flat_accessor_readback() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \ + return JSON.stringify(inner.flat()); \ + })()", + ); + assert_eq!(out, "[1,99]", "flat 经原件续读应见 getter 内逃逸写值"); +} + +/// P3 splice 移位 SET 形态:getter 逃逸写后,移位写经 setter 落原件, +/// 读回链(removed 值、移位后空洞、setter 捕获值)三者与 node 一致。 +#[test] +fn escape_write_splice_shifted_readback() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + var store = 1; \ + Object.defineProperty(inner, 0, { \ + configurable: true, \ + get: function(){ g = inner; g[1] = 99; return store; }, \ + set: function(v){ store = v; } \ + }); \ + var r = inner.splice(0, 1); \ + return JSON.stringify(r) + '|' + inner[1] + '|' + store; \ + })()", + ); + assert_eq!(out, "[1]|undefined|99", "移位 SET 应经原件 setter 落值"); +} + +/// P4 setter 深度:`g[1] = 99` 经 setter 写 this 命名属性,原件与逃逸目标 +/// 均应见 setter 写入。 +#[test] +fn escape_write_setter_depth_original_visible() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \ + Object.defineProperty(inner, 1, { configurable: true, set: function(v){ this._t = v; } }); \ + inner.flat(); \ + return (inner._t === 99) + '|' + (g._t === 99); \ + })()", + ); + assert_eq!(out, "true|true", "setter 写入应落在原件上且双引用互见"); +} + +/// P5 identity 面:逃逸写后大量分配触发执行期晋升,双引用经晋升档 +/// 改写后仍同一对象(晋升不引入克隆分裂)。 +#[test] +fn escape_write_identity_after_in_run_promotion() { + let mut vm = vm_with_threshold(4096); + let out = eval_string( + &mut vm, + "var inner = [1, 2]; \ + g = inner; \ + var big = []; \ + for (var i = 0; i < 50000; i++) big.push({ v: i }); \ + (inner === g) + '|' + inner[1] + '|' + g[1]", + ); + assert_eq!(out, "true|2|2", "执行期晋升后双引用应仍同一对象且值一致"); + assert!(vm.session_gc_stats().total_collections > 0, "应触发执行期收集"); +} + +/// P5 值面对照钉:晋升压载下双引用读回值恒一致(压载只作收集触发器)。 +#[test] +fn escape_write_value_consistent_under_in_run_pressure() { + let mut vm = vm_with_threshold(4096); + let out = eval_string( + &mut vm, + "var inner = [1, 2]; \ + g = inner; \ + var big = []; \ + for (var i = 0; i < 50000; i++) big.push({ v: i }); \ + inner[1] + '|' + g[1]", + ); + assert_eq!(out, "2|2", "压载收集后双引用读回值应一致"); + assert!(vm.session_gc_stats().total_collections > 0, "应触发执行期收集"); +} + +/// P6 for-of 形态:被迭代数组的访问器逃逸写后,迭代续读与迭代后读回一致。 +#[test] +fn escape_write_for_of_iterated_readback() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \ + var s = 0; \ + for (var x of inner) s += x; \ + return s + '|' + inner[1]; \ + })()", + ); + assert_eq!(out, "100|99", "迭代续读应见逃逸写值"); +} + +/// P7 对照钉:Map native 盒直插(键值不经过写屏障),盒内值与原件双态 +/// 恒一致——撤屏障克隆后该面不变。 +#[test] +fn map_native_box_dual_state_consistent() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + var m = new Map(); \ + m.set('k', inner); \ + var h = m.get('k'); \ + h[1] = 99; \ + return inner[1] + '|' + h[1] + '|' + (inner === h); \ + })()", + ); + assert_eq!(out, "99|99|true", "native 盒读回与原件应双态一致"); +} + +/// P8 对照钉:跨 run 边界(reset 路径)session 对象的 epoch 子引用经边界 +/// 修复克隆收敛——双引用跨边界恒同一对象(REPL 两轮语义的 test 形态)。 +#[test] +fn cross_reset_boundary_clone_converges_identity() { + let mut vm = vm_default(); + vm.run(&Arc::new(compile("globalThis.inner = [1, 2]; globalThis.g = globalThis.inner; 0"))) + .expect("run1"); + vm.reset(); + let out = eval_string( + &mut vm, + "globalThis.g[1] = 99; (globalThis.inner[1] === 99) + '|' + (globalThis.inner === globalThis.g)", + ); + assert_eq!(out, "true|true", "跨 reset 边界后双引用应同一对象且值互见"); +} + +/// P9 嵌套元素窗:getter 逃逸写嵌套数组后,flat 递归展开经双路读回一致。 +#[test] +fn escape_write_nested_element_window() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var nested = [7, 8]; \ + var inner = [nested]; \ + Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = nested; g[1] = 99; return g; } }); \ + var r = inner.flat(); \ + return JSON.stringify(r) + '|' + nested[1]; \ + })()", + ); + assert_eq!(out, "[7,99]|99", "嵌套元素窗读回应见逃逸写值"); +} + +/// P10 flatMap 形态:回调结果展开 + 嵌套元素 Get 双窗,逃逸写后经原件读回。 +#[test] +fn escape_write_flat_map_accessor_readback() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \ + return JSON.stringify(inner.flatMap(function(v){ return [v]; })); \ + })()", + ); + assert_eq!(out, "[1,99]", "flatMap 续读应见 getter 内逃逸写值"); +} + +/// P11 对照钉:concat arraylike 无访问器纯赋值窗(length/元素 Get 面), +/// 无逃逸写时结果与 node 逐字一致。 +#[test] +fn concat_arraylike_no_accessor_control() { + let mut vm = vm_default(); + let out = eval_string( + &mut vm, + "(function(){ \ + var inner = [1, 2]; \ + var o = { length: 2, 0: 1, 1: 2 }; \ + return JSON.stringify(inner.concat(o)); \ + })()", + ); + assert_eq!(out, "[1,2,{\"0\":1,\"1\":2,\"length\":2}]", "无访问器窗行为应对照 node"); +}