From d877e63cf11a4a568dbe97080d3b225d5479637a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 13:10:17 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=A8=E5=B1=80=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E6=88=90=E5=91=98=E5=86=99=E4=B8=8E=E5=88=A0=E5=90=8E=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=A7=BD=E5=8F=8D=E5=90=91=E5=90=8C=E6=AD=A5=E9=98=B2?= =?UTF-8?q?=E8=A3=B8=E8=AF=BB=E5=A4=B1=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/object.rs | 6 + crates/oxide_runtime_api/src/lib.rs | 9 ++ crates/oxide_vm/src/dispatch/property.rs | 4 + crates/oxide_vm/src/vm.rs | 31 +++-- crates/oxide_vm/src/vm_props.rs | 101 ++++++++++++++ crates/oxide_vm/src/vm_runtime.rs | 24 ++-- .../tests/global_mirror_sync_tests.rs | 123 ++++++++++++++++++ 7 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 crates/oxide_vm/tests/global_mirror_sync_tests.rs diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index f3b01005..7e6d3470 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -255,6 +255,9 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 all_keys.extend(walk_own_symbol_keys(vm, obj)); } let Some(delete_pos) = all_keys.iter().find(|(si, _)| *si == key_si).map(|(_, pos)| *pos) else { + // 属性缺失:delete 按规范返 true,镜像槽同步 undefined 保持 + // "槽 = A 侧原始存储"不变式(与入口预载缺位语义幂等)。 + vm.sync_global_builtin_mirror(obj, key_si, JsValue::undefined()); return true; }; // walk_own_keys 已返回绝对存储索引(数组含元素区偏移),直接使用。 @@ -319,6 +322,9 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 } } obj.bump_generation(); + // 删除成功:镜像槽同步 undefined(成员形删除 / Reflect.deleteProperty / + // 0x9C 清槽同源收口,后两者对此幂等)。 + vm.sync_global_builtin_mirror(obj, key_si, JsValue::undefined()); true } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 3d0e58c9..fe6f3394 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -177,6 +177,15 @@ pub trait VmHost { &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, ) -> Result<(), String>; fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue); + /// 全局 builtin 属性 A 侧写/删成功后,把当前帧对应镜像槽反向同步为 `val` + ///(维护"槽 = A 侧原始存储"不变式)。 + /// + /// # 边界与前提 + /// - `obj` 非会话全局对象时立即 no-op(接收者指针判等,热路径零额外成本)。 + /// - 键未登记在活动模块 builtin 镜像名集时无槽可写,no-op。 + /// # 副作用 + /// - 写当前帧寄存器文件的镜像槽。 + fn sync_global_builtin_mirror(&mut self, obj: &JsObject, key_si: u32, val: JsValue); // 查找 / 强制转换 fn lookup_str(&self, val: JsValue) -> Option; diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index b13e56b3..87a226aa 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -419,6 +419,9 @@ impl Vm { { self.profiling.record_ic_hit(); ic_trace!("IC_SET hit shape={} slot={} depth={}", cached_shape_id, cached_slot, cached_depth); + // IC 命中快路径防御:今日全局对象带 meta 不可达,防 meta 不变量 + // 被未来优化破坏后镜像静默失步(键由 (shape, slot) 反查)。 + self.sync_global_builtin_mirror_slot(obj, cached_shape_id, cached_slot, cached_depth, value); return Ok(()); } @@ -443,6 +446,7 @@ impl Vm { self.profiling.record_ic_miss(); prop_cache_miss(); obj.set_prop_shape(pos, value); + self.sync_global_builtin_mirror(obj, prop_name_si, value); ic_helper::write_ic_back(self.bytecode_mut(), ic_pc, obj.shape_id(), pos, 0); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index bc08ba90..a9c54298 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -859,6 +859,19 @@ impl Vm { .map(|m| m.as_ref()) } + /// 活动帧所属代际平表里 `active_flat_id` 指向的模块条目(镜像槽同步 / + /// 重载的名集载体)。 + /// + /// # 边界与前提 + /// - 活动代际表在执行期恒在注册表中(首 run 前为预登记的空表占位), + /// `active_flat_id` 越界返回 None(调用方按无模块处理)。 + pub(crate) fn active_module(&self) -> Option<&CompiledModule> { + self.active_table() + .modules + .get(self.active_flat_id as usize) + .map(|m| m.as_ref()) + } + /// 子模块表注册表的当前代际条目总数(run 边界回收行为的测试钉)。 pub fn table_gen_count(&self) -> usize { self.tables.len() @@ -1772,20 +1785,7 @@ impl Vm { self.bytecode = sub_bytecode; self.activate_immutables(gen, sub_idx, &sub.constants); self.cell_stack.push(Vec::with_capacity(sub.cells_needed as usize)); - for (name, reg) in &sub.builtin_reg_map.clone() { - let si = self.kernel_core.perm_interner().intern(name.as_str()).0; - let global = self.session.global_object(); - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { - self.regs[*reg as usize] = global.get_prop_at(pos); - } else { - vm_debug!( - "push_frame: builtin '{}' reg={} NOT on global object (stays {})", - name, - reg, - self.regs[*reg as usize] - ); - } - } + self.reload_builtin_mirror_slots(&sub.builtin_reg_map); self.active_reg_limit = sub_n_registers.max(1); self.pc = 0; @@ -2633,6 +2633,9 @@ impl oxide_runtime_api::VmHost for Vm { fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { self.set_or_create_prop_value(obj, prop_name_si, val) } + fn sync_global_builtin_mirror(&mut self, obj: &JsObject, key_si: u32, val: JsValue) { + self.sync_global_builtin_mirror(obj, key_si, val) + } fn lookup_str(&self, val: JsValue) -> Option { self.lookup_str(val) } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 4b163e71..966a331d 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -152,6 +152,95 @@ impl Vm { None } + /// 全局 builtin 属性 A 侧写成功后反向同步当前帧镜像槽(成员写 / define / + /// delete 成功后调用)。 + /// + /// # 边界与前提 + /// - 仅当接收者为会话全局对象时生效:非全局写经指针判等短路(热路径成本 + /// = 一次指针比较)。 + /// - 名集以活动模块 `builtin_reg_map` 为准(与编译期登记同源):键不在表 + /// 内即无镜像槽,不写。 + /// # 副作用 + /// - 写当前帧寄存器文件镜像槽。 + /// # 注意事项 + /// - 只可挂在真实 A 侧写发生之后;写失败路径(只读 no-op / 抛错)不得 + /// 调用,否则污染镜像槽。 + pub(crate) fn sync_global_builtin_mirror(&mut self, obj: &JsObject, key_si: u32, val: JsValue) { + if !std::ptr::eq(obj as *const JsObject, self.session.global_object().as_ptr()) { + return; + } + let Some(module) = self.active_module() else { + return; + }; + for (name, reg) in &module.builtin_reg_map { + if self.kernel_core.perm_interner().intern(name.as_str()).0 == key_si { + self.regs[*reg as usize] = val; + return; + } + } + } + + /// IC 命中快路径的防御变体(命中分支不解析键):按 (shape, slot) 定位被 + /// 写属性,自镜像名集反查键后同步。 + /// + /// # 边界与前提 + /// - 仅当写落在全局对象自身时生效:原型链目标命中(depth > 0)不改全局 + /// 自身属性,不同步。 + /// - 全局对象自构造起恒带 prop_meta,IC 命中分支对今日不可达;本挂点防 + /// 该不变量被未来优化破坏后镜像静默失步。 + pub(crate) fn sync_global_builtin_mirror_slot( + &mut self, obj: &JsObject, shape_id: u32, slot: u32, depth: u8, val: JsValue, + ) { + if !std::ptr::eq(obj as *const JsObject, self.session.global_object().as_ptr()) { + return; + } + if depth != 0 || shape_id != obj.shape_id() { + return; + } + let Some(module) = self.active_module() else { + return; + }; + for (name, reg) in &module.builtin_reg_map { + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; + if self.kernel_core.shape_forge().lookup_position(shape_id, si) == Some(slot) { + self.regs[*reg as usize] = val; + return; + } + } + } + + /// 重载活动帧模块(`active_flat_id`)的 builtin 镜像槽:先按值取出名集再 + /// 写寄存器,避免借用交叉。帧恢复 / 重执行边界调用。 + pub(crate) fn reload_active_module_mirror_slots(&mut self) { + let map = self.active_module().map(|m| m.builtin_reg_map.clone()).unwrap_or_default(); + self.reload_builtin_mirror_slots(&map); + } + + /// 从全局对象 A 侧重载模块 builtin 名集的镜像槽:属性在位取原始存储值, + /// 缺位写 undefined。 + /// + /// # 边界与前提 + /// - 缺位臂语义为"写 undefined",各入口(run / 帧 / inline / 恢复)统一, + /// 使成员形删除后的裸读与入口预载看到同一值。 + /// # 副作用 + /// - 只写镜像槽寄存器(槽下标受编译期登记约束)。 + pub(crate) fn reload_builtin_mirror_slots(&mut self, map: &[(String, u32)]) { + if map.is_empty() { + return; + } + let global = self.session.global_object(); + for (name, reg) in map { + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; + let val = self + .kernel_core + .shape_forge() + .lookup_position(global.shape_id(), si) + .map(|pos| global.get_prop_at(pos)) + .unwrap_or_else(JsValue::undefined); + self.regs[*reg as usize] = val; + } + } + /// `strict` 为写方(执行赋值的那个函数/脚本)的严格模式:写失败时严格抛 /// TypeError,sloppy 静默 no-op(ECMA-262 OrdinarySetOwnProperty 失败分支)。 pub(crate) fn ordinary_set( @@ -285,6 +374,7 @@ impl Vm { } // pos 是存储索引(get_own_property_slot 对数组已加元素区偏移)。 obj.set_prop_storage(pos as usize, val); + self.sync_global_builtin_mirror(obj, prop_name_si, val); return Ok(()); } @@ -469,11 +559,15 @@ impl Vm { } if crate::ic_helper::ic_set_hit_own(obj, &self.bytecode, ext_pc, val) { self.profiling.record_ic_hit(); + // IC 写命中快路径防御:今日全局对象带 meta 不可达,防不变量被破坏 + // 后静默失步。 + self.sync_global_builtin_mirror(obj, prop_name_si, val); return Ok(()); } self.profiling.record_ic_miss(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_shape(pos, val); + self.sync_global_builtin_mirror(obj, prop_name_si, val); crate::ic_helper::write_ic_back(self.bytecode_mut(), ext_pc, obj.shape_id(), pos, 0); } else if self.named_prop_create_needs_ordinary_set(obj, prop_name_si) { // 数组 length / 整数索引键写新属性:分流回 ordinary_set(ArraySetLength / @@ -533,6 +627,7 @@ impl Vm { obj.set_shape_id(new_shape_id); obj.push_prop(val); obj.bump_generation(); + self.sync_global_builtin_mirror(obj, prop_name_si, val); crate::ic_helper::write_ic_back(self.bytecode_mut(), ext_pc, new_shape_id, slot, 0); self.kernel_core.prop_forge().upsert( new_shape_id, @@ -580,6 +675,7 @@ impl Vm { } if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_shape(pos, val); + self.sync_global_builtin_mirror(obj, prop_name_si, val); } else { // 不可扩展对象禁止新增命名属性(兜底路径,常规入口已拦截)。 if !obj.is_extensible() { @@ -590,6 +686,7 @@ impl Vm { // 数组对象:属性追加到 hash_props 属性区(元素之后),array_prop_count 不变。 obj.push_prop(val); obj.bump_generation(); + self.sync_global_builtin_mirror(obj, prop_name_si, val); } } @@ -662,6 +759,7 @@ impl Vm { obj.set_prop_storage(pos, val); obj.set_data_meta(pos, attributes); obj.bump_generation(); + self.sync_global_builtin_mirror(obj, prop_name_si, val); Ok(()) } @@ -713,6 +811,9 @@ impl Vm { obj.set_prop_storage(pos, JsValue::undefined()); obj.set_accessor_meta(pos, get, set, attributes); obj.bump_generation(); + // accessor 化同步原始存储值(undefined),与入口预载读裸槽同不变式; + // getter 穿透属镜像模型结构残面,不在本挂点扩面。 + self.sync_global_builtin_mirror(obj, prop_name_si, JsValue::undefined()); Ok(()) } diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 8edbbaa7..fb14c252 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -331,6 +331,9 @@ impl Vm { let vm = self; let _window_regs: Vec = Vec::new(); inline_core_fields!(vm, saved, inline_restore, _window_regs); + // 窗口回拷已复活调用方陈旧镜像槽:重载调用方模块的 builtin 名集 + // (active_flat_id 已随宏回写还原到调用方)。 + vm.reload_active_module_mirror_slots(); } /// 首次执行的 VM 就绪:清空执行核心(regs/pc/bytecode/各栈段/迭代器/内联态), @@ -459,13 +462,7 @@ impl Vm { receiver }; self.regs[255] = JsValue::undefined(); - for (name, reg) in &sub.builtin_reg_map { - let si = self.kernel_core.perm_interner().intern(name.as_str()).0; - let global = self.session.global_object(); - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { - self.regs[*reg as usize] = global.get_prop_at(pos); - } - } + self.reload_builtin_mirror_slots(&sub.builtin_reg_map); let _ = callee; vm_trace!( @@ -522,6 +519,9 @@ impl Vm { // active_reg_limit 还原为调用方真实值(caller_reg_limit 可能被存活上界截断)。 self.active_reg_limit = frame.caller_active_reg_limit; self.pc = frame.return_addr; + // 窗口回拷已复活调用方陈旧镜像槽:重载调用方模块的 builtin 名集 + // (active_flat_id 已随保存栈弹回还原到调用方)。 + self.reload_active_module_mirror_slots(); } /// 重新执行当前已加载的 bytecode:清空执行状态并重置 IC 缓存后再次 dispatch。 @@ -535,6 +535,8 @@ impl Vm { self.clear_execution_state(); self.active_reg_limit = self.root_reg_limit; self.regs[254] = self.top_level_this; + // 寄存器文件清空后镜像槽须重载(与 run 入口同语义),否则裸读回陈旧值。 + self.reload_active_module_mirror_slots(); crate::ic_helper::clear_ic_caches(self.bytecode_mut()); self.dispatch() } @@ -606,13 +608,7 @@ impl Vm { self.root_reg_limit = module.n_registers.max(1); self.active_reg_limit = self.root_reg_limit; - for (name, reg) in &module.builtin_reg_map { - let si = self.kernel_core.perm_interner().intern(name.as_str()).0; - let global = self.session.global_object(); - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { - self.regs[*reg as usize] = global.get_prop_at(pos); - } - } + self.reload_builtin_mirror_slots(&module.builtin_reg_map); // 顶层 this:脚本为全局对象(ECMA-262 全局执行上下文); // ES module 顶层环境 GetThisBinding 返回 undefined。记录到 diff --git a/crates/oxide_vm/tests/global_mirror_sync_tests.rs b/crates/oxide_vm/tests/global_mirror_sync_tests.rs new file mode 100644 index 00000000..740ff748 --- /dev/null +++ b/crates/oxide_vm/tests/global_mirror_sync_tests.rs @@ -0,0 +1,123 @@ +//! 成员写 globalThis 后镜像槽反向失步收口测试。 +//! +//! 全局对象双存储:A 侧 = 全局对象属性存储,B 侧 = 镜像寄存器槽(裸标识符 +//! 读走 LOAD_VAR)。成员写 `globalThis.X = v` 只动 A 侧,同 run 内 B 侧须经 +//! 写原语族挂点反向同步;跨调用/跨帧经返回边界重预载。覆盖:同 run 成员写、 +//! 重复写、跨调用、成员删(同 run 与跨调用)、defineProperty 数据描述符形、 +//! 裸名写面不回归、写失败不过同步、三常量拦截不回归、meta 不变量白盒钉。 +//! +//! 核心形断言方向:成员写后裸读应见新属性值,`Math === m`(m 为写前保存) +//! 期望 false;旧行为(槽不刷新)为 true。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + vm.run(&Arc::new(module)) +} + +fn eval_str(source: &str) -> String { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module)).expect("run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + +fn eval_truthy(source: &str) { + let r = eval(source).expect("run"); + assert!(r.is_bool() && r.as_bool(), "expected true, got: {r:?}\nsource: {source}"); +} + +// ── 核心形与重复写:同 run 成员写后裸读见新值 ── +#[test] +fn member_write_same_run_bare_read_sees_new_value() { + eval_truthy("var m = Math; globalThis.Math = {x:1}; Math !== m && Math.x === 1"); +} + +#[test] +fn repeated_member_write_hot_path() { + eval_truthy( + "var m = Math; globalThis.Math = {a:1}; globalThis.Math = {x:2}; \ + Math !== m && Math.x === 2", + ); +} + +// ── 跨调用:调用期 A 侧变更经返回边界重预载 ── +#[test] +fn member_write_across_call() { + eval_truthy("var m = Math; (function(){ globalThis.Math = {x:1}; })(); Math !== m && Math.x === 1"); +} + +#[test] +fn member_delete_across_call_bare_read_undefined() { + eval_truthy( + "var m = Math; (function(){ delete globalThis.Math; })(); \ + Math === undefined && Math !== m", + ); +} + +// ── 成员删同 run:delete 成员形后裸读 undefined ── +#[test] +fn member_delete_same_run_bare_read_undefined() { + eval_truthy("var m = Math; delete globalThis.Math; Math === undefined && Math !== m"); +} + +// ── defineProperty 数据描述符形同收口 ── +#[test] +fn define_property_member_form_same_run() { + eval_truthy( + "var m = Math; Object.defineProperty(globalThis,'Math',\ + {value:{x:1},writable:true,configurable:true,enumerable:false}); \ + Math !== m && Math.x === 1", + ); +} + +// ── 裸名写面不回归 ── +#[test] +fn bare_write_double_write_not_regressed() { + eval_truthy("parseInt = 42; parseInt === 42 && globalThis.parseInt === 42"); +} + +#[test] +fn mixed_bare_then_member_write() { + eval_truthy("var m = parseInt; parseInt = 42; globalThis.parseInt = 43; parseInt === 43"); +} + +// ── 写失败不过同步:只读属性 no-op 写不污染镜像槽 ── +#[test] +fn failed_write_does_not_sync_mirror() { + let s = eval_str("globalThis.undefined = 5; '' + undefined"); + assert_eq!(s, "undefined", "只读 no-op 写不得同步镜像槽,裸 undefined 读回 undefined"); +} + +// ── 三常量裸名拦截不回归 ── +#[test] +fn constant_bare_write_still_intercepted() { + let s = eval_str("undefined = 3; '' + undefined"); + assert_eq!(s, "undefined", "sloppy 裸写三常量静默 no-op"); + let r = eval("(function(){ 'use strict'; undefined = 3; })()"); + assert!(r.is_err(), "strict 裸写三常量须抛 TypeError,got: {r:?}"); +} + +// ── meta 不变量白盒钉:全局对象恒带 prop_meta ── +// 该不变量是 IC 写命中分支对全局对象不可达的前提;若被破坏(全局对象 +// meta-less 化),IC 快路径可达而镜像同步面须重审,此钉先红提示。 +#[test] +fn global_object_always_has_prop_meta() { + let core = oxide_kernel::KernelCore::new(oxide_kernel::KernelConfig::minimal()); + let session = oxide_kernel::KernelSession::new(&core); + assert!( + session.global_object().has_prop_meta(), + "全局对象自构造起恒带 prop_meta(三常量 set_data_meta 落定)" + ); +}