mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: wrapper 本体属性写触发内置重建失效复用
This commit is contained in:
@@ -145,6 +145,10 @@ pub struct BuiltinWorld {
|
||||
///
|
||||
/// 可复用 native 函数 wrapper 带复用键([`FnWrapperKey`]):选择性重建
|
||||
/// 重绑按键命中旧 wrapper,迁移到重建 P 对象槽位,登记表跨重建不增长。
|
||||
///
|
||||
/// 每条目另记洁净世代基线:wrapper 本体被用户写会递增其世代(写的是
|
||||
/// wrapper 自身而非所属 P 对象),基线偏离即视为污染,选择性重建据此
|
||||
/// 失效复用键、强制新建 wrapper。
|
||||
pub(crate) leaked_objects: std::cell::RefCell<Vec<LeakedSlot>>,
|
||||
}
|
||||
|
||||
@@ -168,12 +172,31 @@ impl FnWrapperKey {
|
||||
pub const fn new(family: u16, label: u32, slot: u32, name: u32) -> Self {
|
||||
Self { family, label, slot, name }
|
||||
}
|
||||
|
||||
/// 是否属于 VM 内建原型(generator/async)的站点标签键。
|
||||
///
|
||||
/// 这些站点绑定在 VM 自有的原型 Box 对象上(非本 world 固定 P 字段),
|
||||
/// 故家族为 0 而站点标签非零。其 wrapper 由 VM 内建原型持有,选择性重建
|
||||
/// 不可清键失效:清键会让原型仍引用旧 wrapper 而另建新 wrapper,留下
|
||||
/// 悬垂引用。普通方法 wrapper(P 目标家族非零,或 global/构造器等无标签
|
||||
/// 站点)不在此列,正常参与失效。
|
||||
const fn is_vm_intrinsic_site(&self) -> bool {
|
||||
self.family == 0 && self.label != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 登记表条目:对象指针 + 可选复用键(`None` = 不可复用对象)。
|
||||
///
|
||||
/// `clean_generation` 是登记(或重建收尾经
|
||||
/// [`BuiltinWorld::refresh_leaked_object_baselines`] 刷新)时的对象世代;
|
||||
/// 属性写使其偏离即视为 wrapper 本体污染。`key = None` 除不可复用对象外,
|
||||
/// 还可能是被失效的旧可复用 wrapper——复用键已清空、不再参与重建复用,但
|
||||
/// 本体仍可能被旧引用持有,须滞留至 session 收尾(`teardown_heap_data`)
|
||||
/// 统一释放,不得提前释放。
|
||||
pub(crate) struct LeakedSlot {
|
||||
pub(crate) ptr: *mut JsObject,
|
||||
key: Option<FnWrapperKey>,
|
||||
clean_generation: u32,
|
||||
}
|
||||
|
||||
impl BuiltinWorld {
|
||||
@@ -185,8 +208,17 @@ impl BuiltinWorld {
|
||||
/// 登记一个绑定层经 `Box::into_raw` 持有的函数/宿主对象(不可复用对象),
|
||||
/// 供 [`Self::teardown_heap_data`] 在 session 收尾时统一释放;可复用
|
||||
/// native 函数 wrapper 走 [`Self::track_fn_wrapper`]。
|
||||
///
|
||||
/// 登记点同时读取对象世代作洁净基线(属性写使其偏离即视为本体污染)。
|
||||
#[expect(clippy::not_unsafe_ptr_arg_deref)] // 指针由绑定层保证存活,此函数仅读世代、不转移所有权
|
||||
pub fn track_leaked_object(&self, obj_ptr: *mut JsObject) {
|
||||
self.leaked_objects.borrow_mut().push(LeakedSlot { ptr: obj_ptr, key: None });
|
||||
// SAFETY: obj_ptr 是刚 Box::into_raw 的存活对象,登记点读取其世代作洁净基线。
|
||||
let clean_generation = unsafe { (*obj_ptr).generation() };
|
||||
self.leaked_objects.borrow_mut().push(LeakedSlot {
|
||||
ptr: obj_ptr,
|
||||
key: None,
|
||||
clean_generation,
|
||||
});
|
||||
}
|
||||
|
||||
/// 登记一个可复用 native 函数 wrapper(带复用键),随登记表在 session
|
||||
@@ -195,14 +227,19 @@ impl BuiltinWorld {
|
||||
/// # 注意事项
|
||||
/// 同键重复登记意味着复用键设计缺陷(同家族槽位对应两个不同 wrapper
|
||||
/// 对象)——debug 断言立即失败。
|
||||
#[expect(clippy::not_unsafe_ptr_arg_deref)] // 指针由绑定层保证存活,此函数仅读世代、不转移所有权
|
||||
pub fn track_fn_wrapper(&self, obj_ptr: *mut JsObject, key: FnWrapperKey) {
|
||||
debug_assert!(
|
||||
!self.leaked_objects.borrow().iter().any(|s| s.key == Some(key)),
|
||||
"同键 native 函数 wrapper 重复登记"
|
||||
);
|
||||
self.leaked_objects
|
||||
.borrow_mut()
|
||||
.push(LeakedSlot { ptr: obj_ptr, key: Some(key) });
|
||||
// SAFETY: obj_ptr 是刚装好 length/name 的存活 wrapper,此时世代即洁净基线。
|
||||
let clean_generation = unsafe { (*obj_ptr).generation() };
|
||||
self.leaked_objects.borrow_mut().push(LeakedSlot {
|
||||
ptr: obj_ptr,
|
||||
key: Some(key),
|
||||
clean_generation,
|
||||
});
|
||||
}
|
||||
|
||||
/// 查找复用键相同且 native 函数/参数个数匹配的既有 wrapper(选择性重建
|
||||
@@ -227,6 +264,55 @@ impl BuiltinWorld {
|
||||
.map(|s| s.ptr)
|
||||
}
|
||||
|
||||
/// 释放登记表中是否存在世代偏离洁净基线的条目(wrapper 本体被写)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// 登记表指针 session 存活期内有效。不可复用宿主对象(`Reflect`/`Iterator`
|
||||
/// /`$262`)与其上方法 wrapper 一并纳入检测——它们同样可被用户写,且其
|
||||
/// 所属 global 需要随重建整批换新。
|
||||
pub fn has_dirty_leaked_objects(&self) -> bool {
|
||||
self.leaked_objects.borrow().iter().any(|slot| {
|
||||
// SAFETY: 登记表指针 session 存活期内有效。
|
||||
unsafe { (*slot.ptr).generation() != slot.clean_generation }
|
||||
})
|
||||
}
|
||||
|
||||
/// 使偏离洁净基线的可复用 wrapper 复用键失效(清为 `None`):重建时
|
||||
/// [`Self::find_fn_wrapper`] 落空、改走新建分支,旧 wrapper 不再被复用回
|
||||
/// 新原型槽。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 只清键不释放本体:旧 P 对象在重建收尾前仍可能引用它,本体随 session
|
||||
/// 收尾统一释放;提前释放会双放。
|
||||
/// - keyless 单例本就不可复用,无需清键;VM 内建 generator/async 原型
|
||||
/// wrapper([`FnWrapperKey::is_vm_intrinsic_site`])由 VM 内建原型持有,
|
||||
/// 清键会遗留悬垂引用,同样跳过。
|
||||
pub fn invalidate_dirty_leaked_objects(&self) {
|
||||
for slot in self.leaked_objects.borrow_mut().iter_mut() {
|
||||
let Some(key) = slot.key else { continue };
|
||||
if key.is_vm_intrinsic_site() {
|
||||
continue;
|
||||
}
|
||||
// SAFETY: 登记表指针 session 存活期内有效。
|
||||
if unsafe { (*slot.ptr).generation() != slot.clean_generation } {
|
||||
slot.key = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 以各条目当前世代重刷洁净基线。
|
||||
///
|
||||
/// # 副作用
|
||||
/// 覆盖全部条目的 `clean_generation`;由 [`crate::kernel::KernelSession::record_snapshot`]
|
||||
/// 在重建收尾(原型槽重指与重绑内部写完成)后调用,避免这些内部写在下一次
|
||||
/// 判脏时被误报为 wrapper 本体污染。
|
||||
pub fn refresh_leaked_object_baselines(&self) {
|
||||
for slot in self.leaked_objects.borrow_mut().iter_mut() {
|
||||
// SAFETY: 登记表指针 session 存活期内有效。
|
||||
slot.clean_generation = unsafe { (*slot.ptr).generation() };
|
||||
}
|
||||
}
|
||||
|
||||
/// wrapper 复用键的目标家族标签:目标对象是本 world 固定 P 字段时返回
|
||||
/// 其枚举下标 + 1(`all_p_fields` 顺序跨重建不变),非 P 目标返回 0。
|
||||
pub fn wrapper_family_of(&self, obj: *const JsObject) -> u16 {
|
||||
|
||||
@@ -292,6 +292,37 @@ pub struct BuiltinDirtySet {
|
||||
}
|
||||
|
||||
impl BuiltinDirtySet {
|
||||
/// 全部家族位与 global 均置脏的集合。
|
||||
///
|
||||
/// wrapper 本体被写无法按世代定位到具体家族(写的是 wrapper 自身而非所属
|
||||
/// P 对象),脏检测收缩为全量重建,保证全部复用键都经过失效流程、无旧
|
||||
/// wrapper 被复用回新原型。
|
||||
pub fn all_dirty() -> Self {
|
||||
Self {
|
||||
object: true,
|
||||
array: true,
|
||||
function: true,
|
||||
string: true,
|
||||
number: true,
|
||||
boolean: true,
|
||||
error_family: true,
|
||||
symbol_family: true,
|
||||
math: true,
|
||||
json: true,
|
||||
date: true,
|
||||
set: true,
|
||||
map: true,
|
||||
regexp: true,
|
||||
array_buffer: true,
|
||||
data_view: true,
|
||||
typed_array_family: true,
|
||||
temporal: true,
|
||||
stubs: true,
|
||||
global: true,
|
||||
console: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否存在任何 builtin world 内部对象被污染(不含 global object)。
|
||||
pub fn any_builtin_dirty(&self) -> bool {
|
||||
self.object
|
||||
|
||||
@@ -87,6 +87,9 @@ impl KernelSession {
|
||||
|
||||
/// 重新采集内置对象世代快照,作为下一次脏检查的基准。
|
||||
pub fn record_snapshot(&mut self) {
|
||||
// 重建收尾的原型槽重指与重绑内部写会推进 wrapper 世代:先以当前世代
|
||||
// 重刷释放登记表洁净基线,避免下一次判脏误报 wrapper 本体被写。
|
||||
self.builtin_world.refresh_leaked_object_baselines();
|
||||
self.builtin_snapshot = BuiltinSnapshot::new(&self.builtin_world, &self.global_object);
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ impl KernelSession {
|
||||
let gen = |id: BuiltinId| BuiltinSnapshot::gen(world.get_by_id(id));
|
||||
let snap = |id: BuiltinId| snapshot.generations[id as usize];
|
||||
|
||||
BuiltinDirtySet {
|
||||
let mut dirty = BuiltinDirtySet {
|
||||
object: gen(BuiltinId::ObjectProto) != snap(BuiltinId::ObjectProto)
|
||||
|| gen(BuiltinId::ObjectConstructor) != snap(BuiltinId::ObjectConstructor),
|
||||
array: gen(BuiltinId::ArrayProto) != snap(BuiltinId::ArrayProto)
|
||||
@@ -200,7 +203,14 @@ impl KernelSession {
|
||||
|| gen(BuiltinId::BigIntProto) != snap(BuiltinId::BigIntProto),
|
||||
global: BuiltinSnapshot::gen(&self.global_object) != snapshot.global_object_generation,
|
||||
console: gen(BuiltinId::Console) != snap(BuiltinId::Console),
|
||||
};
|
||||
// wrapper 本体被写不落在任何家族位上(写的是 wrapper 自身而非所属 P
|
||||
// 对象):收缩为全脏,强制重建全部家族与 global,使被写的可复用
|
||||
// wrapper 经失效流程清键、不被复用回新原型。
|
||||
if world.has_dirty_leaked_objects() {
|
||||
dirty = BuiltinDirtySet::all_dirty();
|
||||
}
|
||||
dirty
|
||||
}
|
||||
|
||||
/// 是否自上次快照以来存在任何污染。
|
||||
@@ -226,6 +236,10 @@ impl KernelSession {
|
||||
/// 相比全量重建,可保留未污染的内置对象指针与世代,减少隔离成本。
|
||||
pub fn selective_reset(&mut self, core: &Arc<KernelCore>) -> BuiltinDirtySet {
|
||||
let dirty = self.dirty_since_snapshot();
|
||||
// 被写的可复用 wrapper 复用键先失效:随后重建的 rebind 走 miss 分支
|
||||
// 新建 wrapper,旧 wrapper(含用户新增属性与悬垂对象槽)不被复用回新
|
||||
// 原型,本体滞留至 session 收尾统一释放。
|
||||
self.builtin_world.invalidate_dirty_leaked_objects();
|
||||
if dirty.global {
|
||||
// 旧 global 的属性区在替换前释放,避免旧引用长期持有该内存。
|
||||
let old_global = unsafe { &mut *(self.global_object.as_ptr() as *mut JsObject) };
|
||||
|
||||
@@ -970,6 +970,146 @@ fn full_reset_value_overwrite_via_ic_and_compound_marks_dirty() {
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// wrapper 本体新增 own 属性:写入的是方法 wrapper 自身而非所属 P 对象,
|
||||
/// 只采 P 字段的世代快照无法察觉。修复后释放登记表的洁净基线捕获该写,
|
||||
/// full_reset 失效其复用键并重建家族,属性不跨轮存活、无悬垂对象槽。
|
||||
#[test]
|
||||
fn full_reset_wrapper_own_property_does_not_survive() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
let push_before = method_wrapper_ptr(&vm, vm.session.builtin_world().array_proto.as_ptr(), "push");
|
||||
|
||||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||||
assert!(vm.session.is_dirty_since_snapshot(), "wrapper 本体写应被脏检测捕获");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||||
assert_eq!(run_source(&mut vm, "'custom' in Array.prototype.push"), JsValue::bool(false));
|
||||
assert_eq!(run_source(&mut vm, "typeof Array.prototype.push === 'function'"), JsValue::bool(true));
|
||||
let push_after = method_wrapper_ptr(&vm, vm.session.builtin_world().array_proto.as_ptr(), "push");
|
||||
assert!(!std::ptr::eq(push_before, push_after), "被写的 wrapper 不应被复用");
|
||||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// wrapper 本体写 epoch 对象值:epoch 对象不晋升、不登记,若 wrapper 跨轮
|
||||
/// 存活则槽内指针悬垂。修复后家族重建、旧 wrapper 与槽一并被丢弃。
|
||||
#[test]
|
||||
fn full_reset_wrapper_object_property_does_not_dangle() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
|
||||
let _ = run_source(&mut vm, "Array.prototype.push.custom = {}; 0");
|
||||
assert!(vm.session.is_dirty_since_snapshot(), "wrapper 对象值写应被脏检测捕获");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// wrapper 既有 own 槽的值覆盖:首次写已建槽后重录快照,第二次写走
|
||||
/// `set_prop_storage` 既有槽路径,同样须推进 wrapper 世代被脏检测捕获。
|
||||
#[test]
|
||||
fn full_reset_wrapper_existing_slot_overwrite_marks_dirty() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||||
vm.session.record_snapshot();
|
||||
|
||||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 2; 0");
|
||||
assert!(vm.session.is_dirty_since_snapshot(), "wrapper 既有槽覆盖应被脏检测捕获");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||||
assert_eq!(run_source(&mut vm, "typeof Array.prototype.push === 'function'"), JsValue::bool(true));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// 不可复用宿主对象(`Reflect`)的属性写:登记面无复用键,脏时靠 global
|
||||
/// 重建整批换新宿主 Box,旧宿主连同新增属性一并被丢弃。
|
||||
#[test]
|
||||
fn full_reset_keyless_leaked_object_property_does_not_survive() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
|
||||
let _ = run_source(&mut vm, "Reflect.custom = 1; 0");
|
||||
assert!(vm.session.is_dirty_since_snapshot(), "keyless 宿主对象写应被脏检测捕获");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(run_source(&mut vm, "'custom' in Reflect"), JsValue::bool(false));
|
||||
assert_eq!(run_source(&mut vm, "typeof Reflect.apply === 'function'"), JsValue::bool(true));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// global 函数 wrapper(`parseInt`)的属性写:global 函数 wrapper 无 P 家族
|
||||
/// 归属,脏时必须连带 global 重建并失效其复用键,否则新增属性跨轮存活。
|
||||
#[test]
|
||||
fn full_reset_global_function_wrapper_property_does_not_survive() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
|
||||
let _ = run_source(&mut vm, "parseInt.custom = 1; 0");
|
||||
assert!(vm.session.is_dirty_since_snapshot(), "global 函数 wrapper 写应被脏检测捕获");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(run_source(&mut vm, "'custom' in parseInt"), JsValue::bool(false));
|
||||
assert_eq!(run_source(&mut vm, "parseInt('42')"), JsValue::int(42));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// wrapper 写重建后基线已刷新:后续仅 P 对象脏写的轮次复用新 wrapper、
|
||||
/// 登记表不增长,不因上一轮 wrapper 写残留基线下沉而每轮全量失效。
|
||||
#[test]
|
||||
fn wrapper_write_rebuild_then_p_writes_reuse_fresh_wrapper() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||||
vm.full_reset();
|
||||
|
||||
let push_after = method_wrapper_ptr(&vm, vm.session.builtin_world().array_proto.as_ptr(), "push");
|
||||
let registry_after = vm.session.builtin_world().leaked_object_count();
|
||||
|
||||
for i in 0..3u32 {
|
||||
let source = format!("Array.prototype['w{i}'] = 1; Object.prototype['w{i}'] = 2; 0");
|
||||
let _ = run_source(&mut vm, &source);
|
||||
vm.full_reset();
|
||||
}
|
||||
|
||||
let push_final = method_wrapper_ptr(&vm, vm.session.builtin_world().array_proto.as_ptr(), "push");
|
||||
assert!(std::ptr::eq(push_after, push_final), "P 写轮应复用 wrapper,不应每轮失效重建");
|
||||
assert_eq!(
|
||||
vm.session.builtin_world().leaked_object_count(),
|
||||
registry_after,
|
||||
"P 写轮释放登记表不应增长"
|
||||
);
|
||||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// 假阳性护栏:只写用户对象的良性运行不得误判 wrapper 脏——wrapper 世代
|
||||
/// 仅在自身被写时偏离基线,重建收尾的内部写由快照重刷基线吸收。
|
||||
#[test]
|
||||
fn benign_user_object_writes_do_not_dirty_leaked_objects() {
|
||||
let mut vm = Vm::new();
|
||||
let _ = run_source(&mut vm, "0");
|
||||
let world_ptr = Arc::as_ptr(&vm.session.builtin_world);
|
||||
|
||||
let result = run_source(&mut vm, "(function () { var a = {}; a.x = 1; a.x = 2; return a.x; })()");
|
||||
assert_eq!(result, JsValue::int(2));
|
||||
assert!(!vm.session.is_dirty_since_snapshot(), "只写用户对象不应误判 wrapper 脏");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)), "无污染不应重建 world");
|
||||
assert!(!vm.session.is_dirty_since_snapshot());
|
||||
}
|
||||
|
||||
/// 假阳性护栏:只写用户对象的良性运行不得误判 builtin/global 脏。
|
||||
/// 用对象/数组字面量表达式(对象分配在 epoch、非 session 直分)隔离掉
|
||||
/// full_reset 对 session 对象的既有兜底,从而验证选择性重建未因值写 bump 误触发。
|
||||
|
||||
Reference in New Issue
Block a user