From 951ec21b75108535641e568921e26a6f3b54700e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 18 Sep 2026 02:27:43 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20wrapper=20=E6=9C=AC=E4=BD=93=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E5=86=99=E8=A7=A6=E5=8F=91=E5=86=85=E7=BD=AE=E9=87=8D?= =?UTF-8?q?=E5=BB=BA=E5=A4=B1=E6=95=88=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_kernel/src/builtin/world.rs | 94 ++++++++++++- crates/oxide_kernel/src/kernel/builtin_id.rs | 31 ++++ crates/oxide_kernel/src/kernel/session.rs | 16 ++- crates/oxide_vm/src/vm_support/tests.rs | 140 +++++++++++++++++++ 4 files changed, 276 insertions(+), 5 deletions(-) diff --git a/crates/oxide_kernel/src/builtin/world.rs b/crates/oxide_kernel/src/builtin/world.rs index 34d9993b..431e4d6e 100644 --- a/crates/oxide_kernel/src/builtin/world.rs +++ b/crates/oxide_kernel/src/builtin/world.rs @@ -145,6 +145,10 @@ pub struct BuiltinWorld { /// /// 可复用 native 函数 wrapper 带复用键([`FnWrapperKey`]):选择性重建 /// 重绑按键命中旧 wrapper,迁移到重建 P 对象槽位,登记表跨重建不增长。 + /// + /// 每条目另记洁净世代基线:wrapper 本体被用户写会递增其世代(写的是 + /// wrapper 自身而非所属 P 对象),基线偏离即视为污染,选择性重建据此 + /// 失效复用键、强制新建 wrapper。 pub(crate) leaked_objects: std::cell::RefCell>, } @@ -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, + 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 { diff --git a/crates/oxide_kernel/src/kernel/builtin_id.rs b/crates/oxide_kernel/src/kernel/builtin_id.rs index d53b8c15..6f94e4a1 100644 --- a/crates/oxide_kernel/src/kernel/builtin_id.rs +++ b/crates/oxide_kernel/src/kernel/builtin_id.rs @@ -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 diff --git a/crates/oxide_kernel/src/kernel/session.rs b/crates/oxide_kernel/src/kernel/session.rs index dbd23c1a..a9848f3e 100644 --- a/crates/oxide_kernel/src/kernel/session.rs +++ b/crates/oxide_kernel/src/kernel/session.rs @@ -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) -> 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) }; diff --git a/crates/oxide_vm/src/vm_support/tests.rs b/crates/oxide_vm/src/vm_support/tests.rs index b39f3e13..8358ec55 100644 --- a/crates/oxide_vm/src/vm_support/tests.rs +++ b/crates/oxide_vm/src/vm_support/tests.rs @@ -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 误触发。