fix: 全局对象成员写与删后镜像槽反向同步防裸读失步

This commit is contained in:
2026-09-14 13:10:17 +00:00
parent 4d880c34c9
commit d877e63cf1
7 changed files with 270 additions and 28 deletions
+6
View File
@@ -255,6 +255,9 @@ pub fn delete_own_property<H: VmHost>(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<H: VmHost>(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
}
+9
View File
@@ -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<String>;
+4
View File
@@ -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 {
+17 -14
View File
@@ -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<String> {
self.lookup_str(val)
}
+101
View File
@@ -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(())
}
+10 -14
View File
@@ -331,6 +331,9 @@ impl Vm {
let vm = self;
let _window_regs: Vec<JsValue> = 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。记录到
@@ -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<JsValue, String> {
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 落定)"
);
}