fix: 属性值覆盖同步推进脏代际

This commit is contained in:
2026-09-18 01:41:53 +00:00
parent 46464efede
commit 1be5150667
4 changed files with 124 additions and 5 deletions
+3 -2
View File
@@ -805,12 +805,13 @@ impl JsObject {
Ok(())
}
/// 当前 generation(对象结构变更计数器,IC 失效依据之一)。
/// 当前 generation(对象结构/值变更计数器,脏检测与并发模板裁决依据)。
pub fn generation(&self) -> u32 {
self.generation
}
/// 递增 generation(`wrapping_add`)。
/// 递增 generation(`wrapping_add`)。新增属性、改写既有属性值与原型接线均调用,
/// 快照仅做等值比较,数值大小无语义。
pub fn bump_generation(&mut self) {
self.generation = self.generation.wrapping_add(1);
}
+11
View File
@@ -432,6 +432,9 @@ impl JsObject {
/// 设置数组元素 position 处的值(数组对象);普通对象按绝对下标写入并自动扩容。
/// 数组元素写入会更新 `array_prop_count`(元素数随最高索引增长)。
///
/// # 副作用
/// - 递增本对象 generation:值覆盖属可观察变更,脏检测依赖世代对比。
pub fn set_prop_at(&mut self, position: impl PropIndex, val: JsValue) {
let pos = position.to_u32() as usize;
if pos > MAX_DENSE_PROPS {
@@ -445,6 +448,7 @@ impl JsObject {
let vec = self.ensure_array_elements();
vec[pos] = val;
self.clear_hole_marker(pos);
self.bump_generation();
return;
}
{
@@ -463,6 +467,7 @@ impl JsObject {
meta.push(None);
}
}
self.bump_generation();
}
/// 数组对象(shape 槽位 → 命名属性区存储索引)的属性写入;普通对象等价
@@ -478,6 +483,9 @@ impl JsObject {
/// 按绝对存储索引写入值,数组对象把元素区与命名属性区分派到各自存储。
/// 用于调用方已知属性存储位置(如 `get_own_property_slot` 返回的索引)的场景。
///
/// # 副作用
/// - 递增本对象 generation:值覆盖属可观察变更,脏检测依赖世代对比。
pub fn set_prop_storage(&mut self, idx: usize, val: JsValue) {
if self.is_array() {
let count = self.array_prop_count as usize;
@@ -488,6 +496,7 @@ impl JsObject {
}
vec[idx] = val;
self.clear_hole_marker(idx);
self.bump_generation();
return;
}
let np = idx - count;
@@ -502,6 +511,7 @@ impl JsObject {
meta.push(None);
}
}
self.bump_generation();
return;
}
let vec = self.ensure_hash_props();
@@ -515,6 +525,7 @@ impl JsObject {
meta.push(None);
}
}
self.bump_generation();
}
/// 数组对象属性读取(shape 槽位 → 命名属性区存储索引);普通对象等价
+3 -3
View File
@@ -321,9 +321,9 @@ impl Vm {
///
/// 用于在多次 JS 执行之间达到完全隔离:session 内未被污染的 builtin 保留原指针。
pub fn full_reset(&mut self) {
// session 对象只能来自用户写 + promote,跨 full_reset 若保留 global 会悬垂:
// 覆盖既有 global 槽的写入不递增 generation(脏检测依赖 generation 对比),
// 此处强制 bump 使带 session 对象时 global 必然重建。
// 防御兜底:属性值写原语已推进 generation,常规覆盖写由快照对比发现;
// 若未来出现绕过属性写原语的裸属性区改写,session 对象会随 epoch 释放,
// 保留 global 将持悬垂指针,故带 session 对象时强制 bump 保证 global 重建。
if !self.gc_state.session_object_ptrs.is_empty() {
let global_ptr = self.session.global_object().as_ptr() as *mut JsObject;
unsafe { &mut *global_ptr }.bump_generation();
+107
View File
@@ -884,6 +884,113 @@ fn reset_clears_runtime_state_like_rerun() {
assert!(vm.immutables().is_empty());
}
/// 既有 own 属性值覆盖须推进世代:`Array.prototype.push = 9`(P 对象既有槽、
/// prop_meta 路径)后 full_reset 必须重建 array 家族,函数值恢复。
#[test]
fn full_reset_value_overwrite_on_builtin_proto_rebuilds_family() {
let mut vm = Vm::new();
let _ = run_source(&mut vm, "0");
let old_array_proto = vm.session.builtin_world().array_proto.as_ptr();
assert_eq!(run_source(&mut vm, "Array.prototype.push = 9"), JsValue::int(9));
assert_eq!(run_source(&mut vm, "Array.prototype.push"), JsValue::int(9));
assert!(vm.session.is_dirty_since_snapshot(), "值覆盖应推进世代被脏检测捕获");
vm.full_reset();
assert!(!std::ptr::eq(old_array_proto, vm.session.builtin_world().array_proto.as_ptr()));
let t = run_source(&mut vm, "typeof Array.prototype.push");
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
assert!(!vm.session.is_dirty_since_snapshot());
}
/// 对象值形态的既有槽覆盖同样须被发现:epoch 对象写入 builtin P 对象不被晋升,
/// 若漏 bump 则 full_reset 保留旧槽持悬垂指针(UAF);修复后家族重建、槽被丢弃。
#[test]
fn full_reset_value_overwrite_with_object_value_drops_stale_pointer() {
let mut vm = Vm::new();
let _ = run_source(&mut vm, "0");
let old_array_proto = vm.session.builtin_world().array_proto.as_ptr();
let _ = run_source(&mut vm, "Array.prototype.push = {}; 0");
assert!(vm.session.is_dirty_since_snapshot(), "对象值覆盖应被脏检测捕获");
vm.full_reset();
assert!(!std::ptr::eq(old_array_proto, vm.session.builtin_world().array_proto.as_ptr()));
let t = run_source(&mut vm, "typeof Array.prototype.push");
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
assert!(!vm.session.is_dirty_since_snapshot());
}
/// 零 session 对象的全局内置值覆盖:`globalThis.Array = 9` 不触发 promote,
/// 旧 full_reset 兜底(仅在存在 session 对象时强制 bump global)不生效;
/// 值写原语 bump 后 global 脏,full_reset 重绑回内置构造器。
#[test]
fn full_reset_global_builtin_value_overwrite_restores_constructor() {
let mut vm = Vm::new();
let _ = run_source(&mut vm, "0");
let _ = run_source(&mut vm, "globalThis.Array = 9; 0");
assert!(vm.gc_state.session_object_ptrs.is_empty(), "原始值覆盖不产生 session 对象");
assert!(vm.session.is_dirty_since_snapshot(), "全局值覆盖应推进世代");
vm.full_reset();
assert!(std::ptr::eq(
global_prop(&vm, "Array").as_js_object_ptr(),
vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
assert_eq!(run_source(&mut vm, "[1].push(2)"), JsValue::int(2));
assert!(!vm.session.is_dirty_since_snapshot());
}
/// IC 二次写(miss 落既有槽 + 命中直写)与复合赋值(set_member_prop 既有槽)
/// 两条写路径同样须推进世代,full_reset 后值恢复。
#[test]
fn full_reset_value_overwrite_via_ic_and_compound_marks_dirty() {
let mut vm = Vm::new();
let _ = run_source(&mut vm, "0");
// IC 首写 miss 落既有槽、二次写经缓存命中,两次都须 bump。
let _ = run_source(&mut vm, "Array.prototype.push = 9; Array.prototype.push = 8; 0");
assert!(vm.session.is_dirty_since_snapshot(), "IC 二次写应被脏检测捕获");
vm.full_reset();
let t = run_source(&mut vm, "typeof Array.prototype.push");
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
// 复合赋值读改写走 set_member_prop 既有槽路径,同样须 bump。
let _ = run_source(&mut vm, "Array.prototype.push = 9; Array.prototype.push += 1; 0");
assert!(vm.session.is_dirty_since_snapshot(), "复合赋值应被脏检测捕获");
vm.full_reset();
let t = run_source(&mut vm, "typeof Array.prototype.push");
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
assert_eq!(run_source(&mut vm, "[1, 2, 3].push(4)"), JsValue::int(4));
assert!(!vm.session.is_dirty_since_snapshot());
}
/// 假阳性护栏:只写用户对象的良性运行不得误判 builtin/global 脏。
/// 用对象/数组字面量表达式(对象分配在 epoch、非 session 直分)隔离掉
/// full_reset 对 session 对象的既有兜底,从而验证选择性重建未因值写 bump 误触发。
#[test]
fn benign_run_does_not_falsely_dirty_builtins() {
let mut vm = Vm::new();
let _ = run_source(&mut vm, "0");
let world_ptr = Arc::as_ptr(&vm.session.builtin_world);
let global_ptr = vm.session.global_object.as_ptr();
run_source(&mut vm, "({ x: 1 }).x + [1, 2].length");
assert!(vm.gc_state.session_object_ptrs.is_empty(), "良性表达式不应产生 session 直分对象");
assert!(!vm.session.is_dirty_since_snapshot(), "良性运行不应误判 builtin 脏");
vm.full_reset();
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)), "builtin world 不应重建");
assert!(std::ptr::eq(global_ptr, vm.session.global_object.as_ptr()), "global 不应重建");
assert!(!vm.session.is_dirty_since_snapshot());
}
#[test]
fn full_reset_clears_symbol_state() {
let mut vm = Vm::new();