diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 98ca506f..f20b40d2 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -28,14 +28,14 @@ macro_rules! bind_methods { #[macro_export] macro_rules! bind_methods_static { - ($target:expr, $sf:expr, $sh:expr, $wrapper_proto:expr, + ($target:expr, $sf:expr, $sh:expr, $world:expr, $(($name:literal, $func:expr, $nargs:expr)),* $(,)?) => { $({ let _raw: *const () = $func as *const (); // SAFETY: $func 是 NativeFn 函数项;强转并包装合法。 let _func_ptr = unsafe { oxide_types::object::NativeFnPtr::from_raw(_raw) }; let _ = $crate::builtin::BuiltinWorld::bind_method_static( - $target, $sh, $sf, $name, _func_ptr, $nargs, $wrapper_proto, + $target, $sh, $sf, $name, _func_ptr, $nargs, $world, ); })* }; @@ -297,6 +297,12 @@ pub struct BuiltinWorld { pub async_disposable_stack_proto: P, pub stub_objects: Vec>, pub console_object: P, + /// 绑定层经 `Box::into_raw` 持有的函数/宿主对象登记表(方法 wrapper、 + /// 访问器、错误构造器、Reflect/Iterator、内建原型构造器、`$262` 宿主等)。 + /// 这些对象本体在堆上、不属任何 arena,`session` 收尾时按表统一释放 + /// (属性区 + 本体);选择性重建保留旧 world 时本表随旧 world 整体 + /// 丢弃(泄漏口径与改造前一致,无悬垂风险)。 + leaked_objects: std::cell::RefCell>, } fn intern_label(string_forge: &PermInterner, label: &str) -> u32 { @@ -682,6 +688,160 @@ impl BuiltinWorld { JsValue::from_js_object(self.function_proto.as_ptr() as *mut JsObject) } + /// 登记一个绑定层经 `Box::into_raw` 持有的函数/宿主对象,供 + /// [`Self::teardown_heap_data`] 在 session 收尾时统一释放。 + pub fn track_leaked_object(&self, obj_ptr: *mut JsObject) { + self.leaked_objects.borrow_mut().push(obj_ptr); + } + + /// 选择性重建时把旧 world 的登记表整体并入新 world(见 + /// [`crate::kernel::KernelSession::selective_reset`])。 + pub fn inherit_leaked_objects(&self, from: &BuiltinWorld) { + self.leaked_objects.borrow_mut().append(&mut from.leaked_objects.borrow_mut()); + } + + /// 释放本 world 拥有的全部手工堆数据。 + /// + /// # 口径 + /// 1. `Box::into_raw` 持有的函数/宿主对象(登记表):先释放其堆外属性区, + /// 再释放对象本体; + /// 2. 全部 P 对象字段(原型/构造器家族、迭代器原型族、stub 族、console) + /// 的堆外属性区——对象本体随 Arc 引用归零释放。 + /// + /// # 注意事项 + /// 仅由 session 收尾调用(`KernelSession` 的 `Drop` 与 session 替换前), + /// 幂等:登记表按值取走,属性区释放后置空。选择性重建(dirty rebuild) + /// 不走本路径——旧 world 整体丢弃,登记表与属性区同泄漏(改造前口径), + /// 保留对象的引用不受影响,不引入悬垂。 + pub fn teardown_heap_data(&self) { + for ptr in self.leaked_objects.borrow_mut().drain(..) { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 是绑定层 Box::into_raw 产物,session 存活期内有效, + // 此处恰好释放一次(登记表按值取走,重入时表已空)。 + unsafe { + let obj = &mut *ptr; + obj.release_raw_heap(); + drop(Box::from_raw(ptr)); + } + } + // P 字段逐一枚举:新增字段须在此同步补一行,否则收尾时属性区永久泄漏。 + for p in [ + &self.object_proto, + &self.array_proto, + &self.function_proto, + &self.string_proto, + &self.number_proto, + &self.boolean_proto, + &self.error_proto, + &self.symbol_proto, + &self.object_constructor, + &self.array_constructor, + &self.function_constructor, + &self.string_constructor, + &self.number_constructor, + &self.boolean_constructor, + &self.error_constructor, + &self.symbol_constructor, + &self.type_error_proto, + &self.reference_error_proto, + &self.range_error_proto, + &self.syntax_error_proto, + &self.uri_error_proto, + &self.eval_error_proto, + &self.suppressed_error_proto, + &self.math_object, + &self.json_object, + &self.date_constructor, + &self.date_proto, + &self.set_constructor, + &self.set_proto, + &self.map_constructor, + &self.map_proto, + &self.regexp_constructor, + &self.regexp_proto, + &self.array_buffer_constructor, + &self.array_buffer_proto, + &self.data_view_constructor, + &self.data_view_proto, + &self.typed_array_proto, + &self.typed_array_constructor, + &self.int8array_constructor, + &self.int8array_proto, + &self.uint8array_constructor, + &self.uint8array_proto, + &self.uint8clampedarray_constructor, + &self.uint8clampedarray_proto, + &self.int16array_constructor, + &self.int16array_proto, + &self.uint16array_constructor, + &self.uint16array_proto, + &self.int32array_constructor, + &self.int32array_proto, + &self.uint32array_constructor, + &self.uint32array_proto, + &self.float32array_constructor, + &self.float32array_proto, + &self.float64array_constructor, + &self.float64array_proto, + &self.bigint64array_constructor, + &self.bigint64array_proto, + &self.biguint64array_constructor, + &self.biguint64array_proto, + &self.sym_match, + &self.sym_replace, + &self.sym_search, + &self.sym_split, + &self.sym_iterator, + &self.sym_to_primitive, + &self.sym_has_instance, + &self.sym_match_all, + &self.sym_async_iterator, + &self.sym_to_string_tag, + &self.sym_species, + &self.sym_async_dispose, + &self.sym_dispose, + &self.temporal_object, + &self.temporal_now_object, + &self.instant_constructor, + &self.instant_proto, + &self.plain_date_constructor, + &self.plain_date_proto, + &self.plain_time_constructor, + &self.plain_time_proto, + &self.duration_constructor, + &self.duration_proto, + &self.zoned_date_time_constructor, + &self.zoned_date_time_proto, + &self.plain_date_time_constructor, + &self.plain_date_time_proto, + &self.bigint_constructor, + &self.bigint_proto, + &self.iterator_proto, + &self.array_iterator_proto, + &self.map_iterator_proto, + &self.set_iterator_proto, + &self.string_iterator_proto, + &self.regexp_string_iterator_proto, + &self.iterator_helper_proto, + &self.disposable_stack_proto, + &self.async_disposable_stack_proto, + &self.console_object, + ] { + // SAFETY: p 是本 world 的 P 对象,属性区仅在此释放并置空(幂等)。 + unsafe { + (&mut *p.as_mut_ptr()).release_raw_heap(); + } + } + for p in &self.stub_objects { + // SAFETY: 同上,stub 对象归本 world 所有。 + unsafe { + (&mut *p.as_mut_ptr()).release_raw_heap(); + } + } + } + /// 按 [`BuiltinId`] 取对应内置对象的指针引用。 pub fn get_by_id(&self, id: BuiltinId) -> &P { match id { @@ -951,6 +1111,7 @@ impl BuiltinWorld { async_disposable_stack_proto, stub_objects, console_object, + leaked_objects: std::cell::RefCell::new(Vec::new()), }; wire_builtin_world_links(&world); kernel_info!("BuiltinWorld initialized"); @@ -963,18 +1124,10 @@ impl BuiltinWorld { ) -> BuiltinWorld { let labels = builtin_labels(string_forge); - // 方法 wrapper(Box::into_raw 永久泄漏)的 proto 持有旧 function_proto 裸指针, - // 旧 function_proto 又经 proto/constructor 引用旧 object 家族。function/object - // 重建时这 4 个对象若随 Arc 归零释放,保留 wrapper 会沿悬垂原型链 - // use-after-free:泄漏保活旧家族对,与 wrapper 的永久泄漏同一约定 - // (每次 dirty rebuild 至多泄漏 4 个对象,低频可接受)。 - if dirty.function || dirty.object { - std::mem::forget(current.function_proto.clone()); - std::mem::forget(current.function_constructor.clone()); - std::mem::forget(current.object_proto.clone()); - std::mem::forget(current.object_constructor.clone()); - } - + // 被弃家族的旧 P 对象随旧 world Arc 归零释放;保留 wrapper 的 proto 字段 + // 虽沿旧对象悬空,但 reset 后执行状态已清空、旧对象不在任何指针表, + // 无活引用可达;wrapper 本体经释放表在 session 收尾统一释放, + // 释放路径不读 proto 字段,悬空不触发。 let (object_proto, object_constructor) = if dirty.object { make_named_pair(string_forge, shape_forge, labels, "Object") } else { @@ -1361,6 +1514,7 @@ impl BuiltinWorld { async_disposable_stack_proto, stub_objects, console_object, + leaked_objects: std::cell::RefCell::new(Vec::new()), }; wire_builtin_world_links(&world); world @@ -1487,7 +1641,7 @@ impl BuiltinWorld { "@@iterator", func_ptr, 0, - self.fn_proto_val(), + self, ); debug_assert!(shape_forge.lookup_position(proto.shape_id(), iterator_key).is_some()); } @@ -1629,12 +1783,11 @@ impl BuiltinWorld { ) { let proto_ptr = P::as_ptr(&self.function_proto) as *mut JsObject; let proto = unsafe { &mut *proto_ptr }; - let fp = self.fn_proto_val(); bind_methods_static!( proto, string_forge, shape_forge, - fp, + self, ("call", methods.call, 1), ("apply", methods.apply, 2), ("bind", methods.bind, 1), @@ -1650,7 +1803,7 @@ impl BuiltinWorld { "[Symbol.hasInstance]", unsafe { NativeFnPtr::from_raw(methods.has_instance) }, 1, - fp, + self, ); } @@ -1659,35 +1812,19 @@ impl BuiltinWorld { &self, proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str, native_fn_ptr: NativeFnPtr, arg_count: u8, ) -> Result<(), String> { - Self::bind_method_static( - proto, - shape_forge, - string_forge, - method_name, - native_fn_ptr, - arg_count, - self.fn_proto_val(), - ) + Self::bind_method_static(proto, shape_forge, string_forge, method_name, native_fn_ptr, arg_count, self) } /// 构造并安装一个 native 方法 wrapper 函数对象(设置 `length`/`name` 属性与参数元数据)。 /// - /// 无状态版本,不依赖 `BuiltinWorld` 实例,供静态绑定宏在初始化阶段直接调用。 + /// 无状态版本,供静态绑定宏在初始化阶段直接调用;需要传入当前 + /// `BuiltinWorld`:wrapper 的原型取自它,wrapper 本体登记进它的释放表。 pub fn bind_method_static( proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str, - native_fn_ptr: NativeFnPtr, arg_count: u8, wrapper_proto: JsValue, + native_fn_ptr: NativeFnPtr, arg_count: u8, world: &BuiltinWorld, ) -> Result<(), String> { let si = string_forge.intern(method_name).0; - Self::bind_method_key_static( - proto, - shape_forge, - string_forge, - si, - method_name, - native_fn_ptr, - arg_count, - wrapper_proto, - ) + Self::bind_method_key_static(proto, shape_forge, string_forge, si, method_name, native_fn_ptr, arg_count, world) } /// 按指定属性键安装方法 wrapper(键不要求字符串 intern,well-known symbol 键用此路径)。 @@ -1696,16 +1833,17 @@ impl BuiltinWorld { #[expect(clippy::too_many_arguments)] pub fn bind_method_key_static( proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, key: u32, method_name: &str, - native_fn_ptr: NativeFnPtr, arg_count: u8, wrapper_proto: JsValue, + native_fn_ptr: NativeFnPtr, arg_count: u8, world: &BuiltinWorld, ) -> Result<(), String> { - let wrapper_proto_ptr = if wrapper_proto.is_object() { - wrapper_proto.as_js_object_ptr() + let wrapper_proto_val = world.fn_proto_val(); + let wrapper_proto_ptr = if wrapper_proto_val.is_object() { + wrapper_proto_val.as_js_object_ptr() } else { std::ptr::null_mut() }; let mut wrapper = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); if !wrapper_proto_ptr.is_null() { - wrapper.set_proto(wrapper_proto).ok(); + wrapper.set_proto(wrapper_proto_val).ok(); } wrapper.set_function(true); // NativeFnPtr 不变量由调用方维护(见 bind_method / bind_method_static @@ -1729,7 +1867,10 @@ impl BuiltinWorld { .push(JsValue::perm_string(string_forge.string_ptr(string_forge.intern(method_name).0))); let name_pos = wrapper.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); wrapper.set_data_meta(name_pos, oxide_types::object::PropAttributes::new(false, false, true)); - let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper)); + // 登记进 world 释放表:session 收尾时统一释放 wrapper 的属性区与本体。 + let wrapper_ptr = Box::into_raw(wrapper); + world.track_leaked_object(wrapper_ptr); + let wrapper_val = JsValue::from_js_object(wrapper_ptr); let new_shape = shape_forge.make_shape(proto.shape_id(), key); proto.set_shape_id(new_shape); proto.ensure_hash_props().push(wrapper_val); diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 3a9f5400..77ffbe0e 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -16,7 +16,7 @@ use crate::string_forge::PermInterner; use oxide_log; use oxide_log::{Level, SUBSYSTEM_COUNT}; -/// kernel 运行配置:VM 池规模、步数/调用深度上限、session GC 阈值、 +/// kernel 运行配置:VM 池规模、步数/调用深度/单 run 分配上限、session GC 阈值、 /// 日志级别与内置对象预热开关。由三个预设构造器([`KernelConfig::minimal`] / /// [`KernelConfig::standard`] / [`KernelConfig::full`])或默认值创建。 #[derive(Clone)] @@ -25,6 +25,10 @@ pub struct KernelConfig { pub max_pool_size: Option, pub max_dead_strings: Option, pub max_steps: Option, + /// 单次 run 的分配上限(epoch + session arena + session 堆账目字节): + /// 超限的 run 以 `VM memory limit exceeded` 失败,防单测试 arena 高水位 + /// 拖垮宿主内存。None 为无上限(CLI/嵌入默认;runner 按场景设置)。 + pub max_alloc_bytes: Option, pub max_call_depth: usize, pub session_gc_threshold: usize, pub max_cached_modules: usize, @@ -42,6 +46,7 @@ impl KernelConfig { max_pool_size: Some(8), max_dead_strings: Some(10_000), max_steps: None, + max_alloc_bytes: None, max_call_depth: 1024, session_gc_threshold: 33_554_432, max_cached_modules: 512, @@ -59,6 +64,7 @@ impl KernelConfig { max_pool_size: Some(32), max_dead_strings: Some(10_000), max_steps: None, + max_alloc_bytes: None, max_call_depth: 1024, session_gc_threshold: 33_554_432, max_cached_modules: 512, @@ -76,6 +82,7 @@ impl KernelConfig { max_pool_size: None, max_dead_strings: Some(5_000), max_steps: None, + max_alloc_bytes: None, max_call_depth: 1024, session_gc_threshold: 33_554_432, max_cached_modules: 512, @@ -698,26 +705,53 @@ impl KernelSession { self.dirty_since_snapshot().any() } + /// 收尾释放 session 拥有的手工堆数据:builtin world(方法 wrapper 登记表 + + /// 全部 P 对象属性区)与 global 对象属性区。对象本体随 Arc 引用归零释放。 + /// + /// # 注意事项 + /// 幂等(登记表按值取走、属性区释放后置空),session 生命周期内可安全重入; + /// 仅应在 session 真正终止时调用——选择性重置换出的旧 world/global 不走本路径。 + pub fn teardown_builtins(&mut self) { + self.builtin_world.teardown_heap_data(); + // SAFETY: global 归本 session 所有,收尾时恰好释放其属性区一次。 + let global = unsafe { &mut *(self.global_object.as_ptr() as *mut JsObject) }; + global.release_raw_heap(); + } + /// 选择性重置:只重建被污染的对象(global 或相应 builtin 家族),并返回脏集合。 /// /// 相比全量重建,可保留未污染的内置对象指针与世代,减少隔离成本。 pub fn selective_reset(&mut self, core: &Arc) -> BuiltinDirtySet { let dirty = self.dirty_since_snapshot(); if dirty.global { + // 旧 global 的属性区在替换前释放,避免随旧引用永久泄漏。 + let old_global = unsafe { &mut *(self.global_object.as_ptr() as *mut JsObject) }; + old_global.release_raw_heap(); self.global_object = Self::new_global_object(core); } if dirty.any_builtin_dirty() { - self.builtin_world = Arc::new(BuiltinWorld::rebuild_with_dirty( - &self.builtin_world, + let old_world = self.builtin_world.clone(); + let new_world = BuiltinWorld::rebuild_with_dirty( + &old_world, core.perm_interner.as_ref(), core.shape_forge.as_ref(), &dirty, - )); + ); + // 旧 world 的登记表并入新 world:存活 wrapper(未污染家族别名)仍须在 + // session 收尾统一释放;已弃 wrapper 随之恰好释放一次,不二次持有。 + new_world.inherit_leaked_objects(&old_world); + self.builtin_world = Arc::new(new_world); } dirty } } +impl Drop for KernelSession { + fn drop(&mut self) { + self.teardown_builtins(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index be0ae0b4..5b428b2b 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -870,6 +870,7 @@ fn classify_vm_error(e: &str, neg: Option<&Negative>, no_skip: bool) -> TestOutc || e.contains("not supported") || e.contains("unsupported") || e.contains("step limit") + || e.contains("memory limit") || e.contains("NEW_EXPRESSION") || e.contains("GET_PROP_DYNAMIC on non-object") || e.contains("SET_PROP_DYNAMIC on non-object") @@ -1467,6 +1468,11 @@ fn build_runner_kernel() -> Arc { kernel_config.max_call_depth = 256; kernel_config.min_pool_size = 1; kernel_config.max_pool_size = Some(1); + // 单测试分配上限:死循环类测试触步数上限时持续分配,arena 高水位可达 GB + // 级;多 worker 并发下进程 RSS 包络被各 worker 当前高水位顶起,全量运行 + // 必然 OOM。正规测试峰值 ≤132MB(Array 块 p100 实测),256MiB 留有余量 + // 且把失控测试的驻留面封顶到上限本身。 + kernel_config.max_alloc_bytes = Some(256 * 1024 * 1024); KernelCore::new(kernel_config) } @@ -1509,6 +1515,8 @@ fn categorize_fail(msg: &str) -> (String, String) { ("vm: not yet implemented".into(), String::new()) } else if reason.contains("step limit") { ("vm: step limit".into(), String::new()) + } else if reason.contains("memory limit") { + ("vm: memory limit".into(), String::new()) } else if reason.contains("not defined") { ("vm: not defined".into(), parse_undefined_ident(msg).unwrap_or("").to_string()) } else if reason.contains("unsupported") { @@ -1933,6 +1941,14 @@ mod tests { } } + /// 分配上限超限(与步数上限同款运行期限制):默认 skip,`--no-skip` 下 fail。 + #[test] + fn memory_limit_errors_skip_by_default() { + let e = "vm error: VM memory limit 268435456 exceeded (used 268436480) at pc=14"; + assert_outcome(e, None, false, &TestOutcome::Skip("".into())); + assert_outcome(e, None, true, &TestOutcome::Fail("".into())); + } + /// 能力未实现形态保留 skip;`--no-skip` 下转 fail。 #[test] fn unimplemented_shapes_stay_skipped() { @@ -2030,6 +2046,10 @@ mod tests { ("uncaught ReferenceError: foo is not defined", ("vm: not defined", "foo")), ("compile error: Identifier 'x' is not defined", ("compile: not defined", "x")), ("vm error: IC_GET_PROP on non-object", ("vm: IC_GET_PROP on non-object", "")), + ( + "vm error: VM memory limit 268435456 exceeded (used 268436480) at pc=14", + ("vm: memory limit", ""), + ), ("vm error: TypeError: method called on incompatible receiver", ("vm: other", "")), ("engine panic: boom", ("engine panic", "")), ( diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index 56b1be0e..459c8a18 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -32,6 +32,12 @@ impl P { pub fn as_mut_ptr(&self) -> *mut T { self.as_ptr() as *mut T } + + /// 当前强引用数。收尾路径据此区分「本 VM 独占」与「与 world 等共享」 + /// 的 P 对象,避免对仍被其他引用方使用的对象做归属误判。 + pub fn strong_count(&self) -> usize { + Arc::strong_count(&self.0) + } } impl Clone for P { diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index cce106b9..a540b36e 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -959,6 +959,34 @@ impl JsObject { self.prop_meta } + /// 释放四处堆外裸指针区(命名属性向量/元数据、数组元素区/元素元数据)并置空。 + /// + /// 供 session 收尾调用:对象本体可能仍被 Arc 引用(此后属性区不再被读取), + /// 每区至多释放一次、重复调用为 no-op。upvalue 列表不在本函数口径内 + /// (原件与晋升克隆间别名,须收尾时去重统一释放)。 + pub fn release_raw_heap(&mut self) { + // SAFETY: 四个区各由 ensure_* 路径经 Box::into_raw 分配一次(或为空指针); + // Box::from_raw 回收后即刻置空,杜绝二次释放。 + unsafe { + if !self.array_elements.is_null() { + drop(Box::from_raw(self.array_elements as *mut Vec)); + self.array_elements = std::ptr::null_mut(); + } + if !self.array_elements_meta.is_null() { + drop(Box::from_raw(self.array_elements_meta as *mut Vec>)); + self.array_elements_meta = std::ptr::null_mut(); + } + if !self.hash_props.is_null() { + drop(Box::from_raw(self.hash_props as *mut Vec)); + self.hash_props = std::ptr::null_mut(); + } + if !self.prop_meta.is_null() { + drop(Box::from_raw(self.prop_meta as *mut Vec>)); + self.prop_meta = std::ptr::null_mut(); + } + } + } + /// 原生 / 外来对象 payload 指针。 pub fn native_data(&self) -> *mut u8 { self.native_data diff --git a/crates/oxide_vm/src/async_func.rs b/crates/oxide_vm/src/async_func.rs index 968fd4c8..ee7311f4 100644 --- a/crates/oxide_vm/src/async_func.rs +++ b/crates/oxide_vm/src/async_func.rs @@ -12,7 +12,6 @@ use std::sync::Arc; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_boolean, NativeResult}; -use oxide_types::mem::P; use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; @@ -420,11 +419,13 @@ pub(crate) fn init_async_intrinsics(vm: &mut Vm) { let proto_si = sf.intern("prototype").0; let ctor_shape2 = sh.make_shape(af_ctor.shape_id(), proto_si); af_ctor.set_shape_id(ctor_shape2); - // af_proto.constructor = %AsyncFunction%(构造器对象泄漏持有,与 builtin 方法 wrapper 同生命周期)。 + // af_proto.constructor = %AsyncFunction%(构造器登记进 world 释放表,与 builtin 方法 wrapper 同生命周期)。 let ctor_si = sf.intern("constructor").0; let pshape = sh.make_shape(af_proto.shape_id(), ctor_si); af_proto.set_shape_id(pshape); + // 登记进释放表:session 收尾统一释放构造器本体与属性区。 let af_ctor_ptr = Box::into_raw(af_ctor); + vm.session.builtin_world().track_leaked_object(af_ctor_ptr); let cpos = af_proto.push_prop(JsValue::from_js_object(af_ctor_ptr)); af_proto.set_data_meta(cpos, PropAttributes::new(false, false, true)); // af_proto[Symbol.toStringTag] = "AsyncFunction"(数据属性,w/e/c = false/false/true)。 @@ -437,8 +438,8 @@ pub(crate) fn init_async_intrinsics(vm: &mut Vm) { // proto 本体只存在于 P 槽(Arc 副本,原 Box 随函数结束释放):装入 P 槽后再把 // 构造器 prototype 属性指向 P 槽实例,使其与动态异步函数使用的 [[Prototype]] // 同一对象,保证 `AsyncFunction.prototype === (async () => {}).__proto__`。 - vm.async_function_proto = P::new(*af_proto); - // SAFETY: af_ctor_ptr 为 Box 原分配(泄漏持有、生命周期覆盖 session),对象已建满、本 Vm 独占。 + Vm::swap_intrinsic_proto(&mut vm.async_function_proto, *af_proto); + // SAFETY: af_ctor_ptr 为 Box 原分配(已登记释放表、生命周期覆盖 session),对象已建满、本 Vm 独占。 unsafe { let ctor_mut = &mut *af_ctor_ptr; let ppos = ctor_mut.push_prop(JsValue::from_js_object(vm.async_function_proto.as_mut_ptr())); diff --git a/crates/oxide_vm/src/async_generator.rs b/crates/oxide_vm/src/async_generator.rs index 5fcad811..9bd112cc 100644 --- a/crates/oxide_vm/src/async_generator.rs +++ b/crates/oxide_vm/src/async_generator.rs @@ -18,7 +18,6 @@ use std::sync::Arc; use oxide_builtins::iterator::make_iter_result; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::NativeResult; -use oxide_types::mem::P; use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; @@ -734,6 +733,7 @@ pub(crate) fn init_async_generator_intrinsics(vm: &mut Vm) { let sf = vm.kernel_core.perm_interner().as_ref(); let sh = vm.kernel_core.shape_forge().as_ref(); let fn_proto_val = vm.session.builtin_world().fn_proto_val(); + let world = vm.session.builtin_world(); let object_proto_val = JsValue::from_js_object(vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject); // %AsyncGeneratorPrototype%:proto = Object.prototype,方法 next/return/throw。 @@ -742,7 +742,7 @@ pub(crate) fn init_async_generator_intrinsics(vm: &mut Vm) { &mut ag_proto, sf, sh, - fn_proto_val, + world, ("next", async_generator_next as *const (), 1), ("return", async_generator_return as *const (), 1), ("throw", async_generator_throw as *const (), 1), @@ -763,9 +763,9 @@ pub(crate) fn init_async_generator_intrinsics(vm: &mut Vm) { "@@asyncIterator", unsafe { oxide_types::object::NativeFnPtr::from_raw(async_generator_symbol_async_iterator as *const ()) }, 0, - fn_proto_val, + world, ); - vm.async_generator_proto = P::new(*ag_proto); + Vm::swap_intrinsic_proto(&mut vm.async_generator_proto, *ag_proto); // %AsyncGeneratorFunction.prototype%:proto = Function.prototype,constructor = %AsyncGeneratorFunction%。 let mut agf_ctor = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); @@ -781,11 +781,13 @@ pub(crate) fn init_async_generator_intrinsics(vm: &mut Vm) { let name_si = sf.intern("name").0; let name_shape = sh.make_shape(agf_ctor.shape_id(), name_si); agf_ctor.set_shape_id(name_shape); - // agf_proto.constructor = %AsyncGeneratorFunction%(构造器对象泄漏持有,与 builtin 方法 wrapper 同生命周期)。 + // agf_proto.constructor = %AsyncGeneratorFunction%(构造器登记进 world 释放表,与 builtin 方法 wrapper 同生命周期)。 let ctor_si = sf.intern("constructor").0; let ctor2_shape = sh.make_shape(agf_proto.shape_id(), ctor_si); agf_proto.set_shape_id(ctor2_shape); + // 登记进释放表:session 收尾统一释放构造器本体与属性区。 let agf_ctor_ptr = Box::into_raw(agf_ctor); + world.track_leaked_object(agf_ctor_ptr); let cpos = agf_proto.push_prop(JsValue::from_js_object(agf_ctor_ptr)); agf_proto.set_data_meta(cpos, PropAttributes::new(false, false, true)); // agf_proto.prototype = %AsyncGeneratorPrototype%。 @@ -804,8 +806,8 @@ pub(crate) fn init_async_generator_intrinsics(vm: &mut Vm) { // proto 本体只存在于 P 槽(Arc 副本,原 Box 随函数结束释放):装入 P 槽后再写 // 构造器 prototype/name 属性(按模板序 prototype 在前),prototype 指向 P 槽实例, // 使其与动态异步生成器函数使用的 [[Prototype]] 同一对象。 - vm.async_generator_function_proto = P::new(*agf_proto); - // SAFETY: agf_ctor_ptr 为 Box 原分配(泄漏持有、生命周期覆盖 session),对象已建满、本 Vm 独占。 + Vm::swap_intrinsic_proto(&mut vm.async_generator_function_proto, *agf_proto); + // SAFETY: agf_ctor_ptr 为 Box 原分配(已登记释放表、生命周期覆盖 session),对象已建满、本 Vm 独占。 unsafe { let ctor_mut = &mut *agf_ctor_ptr; let ppos = ctor_mut.push_prop(JsValue::from_js_object(vm.async_generator_function_proto.as_mut_ptr())); diff --git a/crates/oxide_vm/src/bindings/bind_async_disposable_stack.rs b/crates/oxide_vm/src/bindings/bind_async_disposable_stack.rs index a94c4df7..94fc52c7 100644 --- a/crates/oxide_vm/src/bindings/bind_async_disposable_stack.rs +++ b/crates/oxide_vm/src/bindings/bind_async_disposable_stack.rs @@ -117,7 +117,10 @@ pub fn bind_async_disposable_stack(core: &Arc, session: &KernelSessi ctor.set_data_meta(0u32, PropAttributes::new(false, false, false)); ctor.set_data_meta(1u32, PropAttributes::new(false, false, true)); ctor.set_data_meta(2u32, PropAttributes::new(false, false, true)); - JsValue::from_js_object(Box::into_raw(ctor)) + let ctor_ptr = Box::into_raw(ctor); + // 登记进 world 释放表:session 收尾统一释放构造器本体与属性区。 + world.track_leaked_object(ctor_ptr); + JsValue::from_js_object(ctor_ptr) }; write_proto_constructor(core, proto, ctor_val); diff --git a/crates/oxide_vm/src/bindings/bind_disposable_stack.rs b/crates/oxide_vm/src/bindings/bind_disposable_stack.rs index e5610948..9803441c 100644 --- a/crates/oxide_vm/src/bindings/bind_disposable_stack.rs +++ b/crates/oxide_vm/src/bindings/bind_disposable_stack.rs @@ -135,7 +135,10 @@ pub fn bind_disposable_stack(core: &Arc, session: &KernelSession, gl ctor.set_data_meta(0u32, PropAttributes::new(false, false, false)); ctor.set_data_meta(1u32, PropAttributes::new(false, false, true)); ctor.set_data_meta(2u32, PropAttributes::new(false, false, true)); - JsValue::from_js_object(Box::into_raw(ctor)) + let ctor_ptr = Box::into_raw(ctor); + // 登记进 world 释放表:session 收尾统一释放构造器本体与属性区。 + world.track_leaked_object(ctor_ptr); + JsValue::from_js_object(ctor_ptr) }; write_proto_constructor(core, proto, ctor_val); diff --git a/crates/oxide_vm/src/bindings/bind_error.rs b/crates/oxide_vm/src/bindings/bind_error.rs index 599a4bc6..08b09bed 100644 --- a/crates/oxide_vm/src/bindings/bind_error.rs +++ b/crates/oxide_vm/src/bindings/bind_error.rs @@ -43,6 +43,8 @@ fn bind_error_subtype_constructor( ctor.set_data_meta(2u32, PropAttributes::new(false, false, true)); let ctor_ptr = Box::into_raw(ctor); + // 登记进 world 释放表:session 收尾统一释放构造器本体与属性区。 + session.builtin_world().track_leaked_object(ctor_ptr); let proto = unsafe { &mut *proto_ptr }; let proto_ctor_shape = sh.make_shape(proto.shape_id(), sf.intern("constructor").0); diff --git a/crates/oxide_vm/src/bindings/bind_iterator.rs b/crates/oxide_vm/src/bindings/bind_iterator.rs index 4f5eef93..0447a599 100644 --- a/crates/oxide_vm/src/bindings/bind_iterator.rs +++ b/crates/oxide_vm/src/bindings/bind_iterator.rs @@ -34,5 +34,8 @@ pub fn bind_iterator(core: &Arc, session: &KernelSession, global: &m &[("from", oxide_builtins::iterator::iterator_from:: as *const (), 1)], ); - bind_global_value(core, global, "Iterator", JsValue::from_js_object(Box::into_raw(iterator))); + let iterator_ptr = Box::into_raw(iterator); + // 登记进 world 释放表:session 收尾统一释放构造器本体与属性区。 + session.builtin_world().track_leaked_object(iterator_ptr); + bind_global_value(core, global, "Iterator", JsValue::from_js_object(iterator_ptr)); } diff --git a/crates/oxide_vm/src/bindings/bind_reflect.rs b/crates/oxide_vm/src/bindings/bind_reflect.rs index caca73f7..2b30d4b6 100644 --- a/crates/oxide_vm/src/bindings/bind_reflect.rs +++ b/crates/oxide_vm/src/bindings/bind_reflect.rs @@ -62,5 +62,8 @@ pub fn bind_reflect(core: &Arc, session: &KernelSession, global: &mu ], ); - bind_global_value(core, global, "Reflect", JsValue::from_js_object(Box::into_raw(reflect))); + let reflect_ptr = Box::into_raw(reflect); + // 登记进 world 释放表:session 收尾统一释放对象本体与属性区。 + session.builtin_world().track_leaked_object(reflect_ptr); + bind_global_value(core, global, "Reflect", JsValue::from_js_object(reflect_ptr)); } diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index e10913ba..ce1b28de 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -129,14 +129,15 @@ pub(crate) fn apply_binding_table( /// 2. 为属性名开 shape 槽位并写入访问器 meta。 /// /// # 注意事项 -/// getter 函数对象经 `Box::into_raw` 持有,与 `bind_method` 的方法 wrapper 同一生命周期约定 -/// (内置对象在 session 生命周期内不被回收)。 +/// getter 函数对象登记进 world 释放表,与 `bind_method` 的方法 wrapper 同一生命周期约定 +/// (session 收尾时统一释放)。 pub(crate) fn bind_accessor_getter( core: &Arc, session: &KernelSession, proto: &mut JsObject, name: &str, getter_fn: *const (), ) { let shape_forge = core.shape_forge().as_ref(); let string_forge = core.perm_interner().as_ref(); - let fn_proto_val = JsValue::from_js_object(session.builtin_world().function_proto.as_ptr() as *mut JsObject); + let world = session.builtin_world(); + let fn_proto_val = JsValue::from_js_object(world.function_proto.as_ptr() as *mut JsObject); let getter_name = format!("get {name}"); let mut getter = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); @@ -161,7 +162,9 @@ pub(crate) fn bind_accessor_getter( let length_pos = getter.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); getter.set_data_meta(length_pos, PropAttributes::new(false, false, true)); - let getter_val = JsValue::from_js_object(Box::into_raw(getter)); + let getter_ptr = Box::into_raw(getter); + world.track_leaked_object(getter_ptr); + let getter_val = JsValue::from_js_object(getter_ptr); let si = string_forge.intern(name).0; let new_shape = shape_forge.make_shape(proto.shape_id(), si); @@ -180,8 +183,8 @@ pub(crate) fn bind_accessor_getter( /// 2. 为键开 shape 槽位并写入访问器 meta(enumerable=false, configurable=true)。 /// /// # 注意事项 -/// getter/setter 函数对象经 `Box::into_raw` 持有,与 `bind_accessor_getter` 的 -/// 方法 wrapper 同一生命周期约定(内置对象在 session 生命周期内不被回收)。 +/// getter/setter 函数对象登记进 world 释放表,session 收尾时统一释放, +/// 与 `bind_accessor_getter` 的 getter 及方法 wrapper 同一生命周期约定。 #[expect(clippy::too_many_arguments)] pub(crate) fn bind_accessor_getset( core: &Arc, session: &KernelSession, proto: &mut JsObject, key: u32, getter_name: &str, @@ -189,7 +192,8 @@ pub(crate) fn bind_accessor_getset( ) { let shape_forge = core.shape_forge().as_ref(); let string_forge = core.perm_interner().as_ref(); - let fn_proto_val = JsValue::from_js_object(session.builtin_world().function_proto.as_ptr() as *mut JsObject); + let world = session.builtin_world(); + let fn_proto_val = JsValue::from_js_object(world.function_proto.as_ptr() as *mut JsObject); let mut getter = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); getter.set_function(true); @@ -211,7 +215,9 @@ pub(crate) fn bind_accessor_getset( getter.ensure_hash_props().push(JsValue::int(0)); let length_pos = getter.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); getter.set_data_meta(length_pos, PropAttributes::new(false, false, true)); - let getter_val = JsValue::from_js_object(Box::into_raw(getter)); + let getter_ptr = Box::into_raw(getter); + world.track_leaked_object(getter_ptr); + let getter_val = JsValue::from_js_object(getter_ptr); let mut setter = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); setter.set_function(true); @@ -230,7 +236,9 @@ pub(crate) fn bind_accessor_getset( setter.ensure_hash_props().push(JsValue::int(1)); let length_pos = setter.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); setter.set_data_meta(length_pos, PropAttributes::new(false, false, true)); - let setter_val = JsValue::from_js_object(Box::into_raw(setter)); + let setter_ptr = Box::into_raw(setter); + world.track_leaked_object(setter_ptr); + let setter_val = JsValue::from_js_object(setter_ptr); // 访问器属性槽:enumerable=false、configurable=true(get/set 函数对象已持有)。 let new_shape = shape_forge.make_shape(proto.shape_id(), key); @@ -301,7 +309,7 @@ pub(crate) fn bind_well_known_method( method_name, fn_ptr, nargs, - world.fn_proto_val(), + world, ); } @@ -617,6 +625,7 @@ fn bind_error_subtype_global( ctor.set_data_meta(1u32, PropAttributes::new(false, false, true)); let ctor_ptr = Box::into_raw(ctor); + session.builtin_world().track_leaked_object(ctor_ptr); bind_existing_global(core, global, name, JsValue::from_js_object(ctor_ptr)); } @@ -702,7 +711,9 @@ fn bind_reflect_global(core: &Arc, session: &KernelSession, global: ), ], ); - bind_existing_global(core, global, "Reflect", JsValue::from_js_object(Box::into_raw(reflect))); + let reflect_ptr = Box::into_raw(reflect); + session.builtin_world().track_leaked_object(reflect_ptr); + bind_existing_global(core, global, "Reflect", JsValue::from_js_object(reflect_ptr)); } fn bind_iterator_global(core: &Arc, session: &KernelSession, global: &mut JsObject) { @@ -722,7 +733,9 @@ fn bind_iterator_global(core: &Arc, session: &KernelSession, global: core, &[("from", oxide_builtins::iterator::iterator_from:: as *const (), 1)], ); - bind_existing_global(core, global, "Iterator", JsValue::from_js_object(Box::into_raw(iterator))); + let iterator_ptr = Box::into_raw(iterator); + session.builtin_world().track_leaked_object(iterator_ptr); + bind_existing_global(core, global, "Iterator", JsValue::from_js_object(iterator_ptr)); // 迭代器原型方法(%IteratorPrototype% 的 @@iterator 与各集合原型 next)由 // `bind_iterator_protos` 在 object 家族重建时统一安装,这里不重复绑定, // 避免保留原型经 dirty reset 时属性槽膨胀。 diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index 87618b4a..212c8ca7 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use oxide_builtins::iterator::{is_callable, make_iter_result}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_boolean, NativeResult}; -use oxide_types::mem::P; use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; @@ -728,6 +727,7 @@ pub(crate) fn init_generator_intrinsics(vm: &mut Vm) { let sf = vm.kernel_core.perm_interner().as_ref(); let sh = vm.kernel_core.shape_forge().as_ref(); let fn_proto_val = vm.session.builtin_world().fn_proto_val(); + let world = vm.session.builtin_world(); // %GeneratorPrototype%:proto = %IteratorPrototype%(生成器是迭代器,继承 // @@iterator 与 Iterator helper 方法),方法 next/return/throw。 let iterator_proto_val = @@ -737,7 +737,7 @@ pub(crate) fn init_generator_intrinsics(vm: &mut Vm) { &mut gen_proto, sf, sh, - fn_proto_val, + world, ("next", generator_next as *const (), 1), ("return", generator_return as *const (), 1), ("throw", generator_throw as *const (), 1), @@ -758,9 +758,9 @@ pub(crate) fn init_generator_intrinsics(vm: &mut Vm) { "@@iterator", unsafe { oxide_types::object::NativeFnPtr::from_raw(generator_symbol_iterator as *const ()) }, 0, - fn_proto_val, + world, ); - vm.generator_proto = P::new(*gen_proto); + Vm::swap_intrinsic_proto(&mut vm.generator_proto, *gen_proto); // %GeneratorFunction.prototype%:proto = Function.prototype,constructor = %GeneratorFunction%。 let mut gf_ctor = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); @@ -776,11 +776,13 @@ pub(crate) fn init_generator_intrinsics(vm: &mut Vm) { let name_si = sf.intern("name").0; let name_shape = sh.make_shape(gf_ctor.shape_id(), name_si); gf_ctor.set_shape_id(name_shape); - // gf_proto.constructor = gf_ctor(构造器对象泄漏持有,与 builtin 方法 wrapper 同生命周期)。 + // gf_proto.constructor = gf_ctor(构造器登记进 world 释放表,与 builtin 方法 wrapper 同生命周期)。 let ctor_si = sf.intern("constructor").0; let ctor2_shape = sh.make_shape(gf_proto.shape_id(), ctor_si); gf_proto.set_shape_id(ctor2_shape); + // 登记进释放表:session 收尾统一释放构造器本体与属性区。 let gf_ctor_ptr = Box::into_raw(gf_ctor); + world.track_leaked_object(gf_ctor_ptr); let cpos = gf_proto.push_prop(JsValue::from_js_object(gf_ctor_ptr)); gf_proto.set_data_meta(cpos, oxide_types::object::PropAttributes::new(false, false, true)); // gf_proto.prototype = %GeneratorPrototype%(默认原型,default-proto 测试读取)。 @@ -799,8 +801,8 @@ pub(crate) fn init_generator_intrinsics(vm: &mut Vm) { // proto 本体只存在于 P 槽(Arc 副本,原 Box 随函数结束释放):装入 P 槽后再写 // 构造器 prototype/name 属性(按模板序 prototype 在前),prototype 指向 P 槽实例, // 使其与动态生成器函数使用的 [[Prototype]] 同一对象。 - vm.generator_function_proto = P::new(*gf_proto); - // SAFETY: gf_ctor_ptr 为 Box 原分配(泄漏持有、生命周期覆盖 session),对象已建满、本 Vm 独占。 + Vm::swap_intrinsic_proto(&mut vm.generator_function_proto, *gf_proto); + // SAFETY: gf_ctor_ptr 为 Box 原分配(已登记释放表、生命周期覆盖 session),对象已建满、本 Vm 独占。 unsafe { let ctor_mut = &mut *gf_ctor_ptr; let ppos = ctor_mut.push_prop(JsValue::from_js_object(vm.generator_function_proto.as_mut_ptr())); diff --git a/crates/oxide_vm/src/promise.rs b/crates/oxide_vm/src/promise.rs index d53a0469..622375d2 100644 --- a/crates/oxide_vm/src/promise.rs +++ b/crates/oxide_vm/src/promise.rs @@ -9,7 +9,6 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::NativeResult; -use oxide_types::mem::P; use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::private_key::make_int_key; use oxide_types::value::JsValue; @@ -705,6 +704,7 @@ impl Vm { let sf = self.kernel_core.perm_interner().as_ref(); let sh = self.kernel_core.shape_forge().as_ref(); let fn_proto_val = self.session.builtin_world().fn_proto_val(); + let world = self.session.builtin_world(); let object_proto_val = JsValue::from_js_object(self.session.builtin_world().object_proto.as_ptr() as *mut JsObject); @@ -719,7 +719,7 @@ impl Vm { &mut proto, sf, sh, - fn_proto_val, + world, ("then", promise_then as *const (), 2), ("catch", promise_catch as *const (), 1), ("finally", promise_finally as *const (), 1), @@ -757,7 +757,7 @@ impl Vm { &mut ctor, sf, sh, - fn_proto_val, + world, ("resolve", promise_static_resolve as *const (), 1), ("reject", promise_static_reject as *const (), 1), ("all", promise_static_all as *const (), 1), @@ -768,8 +768,8 @@ impl Vm { ); // 固定地址后互相接线:proto.constructor ↔ ctor.prototype。 - self.promise_proto = P::new(*proto); - self.promise_constructor = P::new(*ctor); + Self::swap_intrinsic_proto(&mut self.promise_proto, *proto); + Self::swap_intrinsic_proto(&mut self.promise_constructor, *ctor); let proto_mut = unsafe { &mut *self.promise_proto.as_mut_ptr() }; proto_mut.set_prop_at(0u32, JsValue::from_js_object(self.promise_constructor.as_ptr() as *mut JsObject)); let ctor_mut = unsafe { &mut *self.promise_constructor.as_mut_ptr() }; @@ -1169,8 +1169,8 @@ impl Vm { ctor.set_data_meta(2u32, PropAttributes::new(false, false, false)); // 固定地址后互相接线:proto.constructor ↔ ctor.prototype。 - self.aggregate_error_proto = P::new(*proto); - self.aggregate_error_constructor = P::new(*ctor); + Self::swap_intrinsic_proto(&mut self.aggregate_error_proto, *proto); + Self::swap_intrinsic_proto(&mut self.aggregate_error_constructor, *ctor); let proto_mut = unsafe { &mut *self.aggregate_error_proto.as_mut_ptr() }; proto_mut .set_prop_at(0u32, JsValue::from_js_object(self.aggregate_error_constructor.as_ptr() as *mut JsObject)); diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index e7620892..e9a66284 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -54,8 +54,9 @@ impl SessionGc { } } - /// 只读核算 session 对象的堆数据字节(属性/元素/meta Vec capacity + native 状态盒)。 - /// 与 `drop_object_heap_data` 释放口径一致(capacity),不释放、不置空任何指针。 + /// 只读核算 session 对象的堆数据字节(属性/元素/meta Vec capacity + upvalue 列表 + /// capacity + native 状态盒)。与收尾释放口径一致(capacity);upvalue 列表因原件与 + /// 晋升克隆别名,释放走收尾集中去重而非逐对象路径。不释放、不置空任何指针。 pub(crate) fn object_heap_data_bytes(obj: &JsObject) -> u64 { let mut bytes = 0u64; @@ -89,6 +90,14 @@ impl SessionGc { } } + let upvalues_ptr = obj.upvalues as *const Vec<*mut oxide_types::object::Cell>; + if !upvalues_ptr.is_null() { + unsafe { + bytes += size_of::>() as u64 + + ((*upvalues_ptr).capacity() * size_of::<*mut oxide_types::object::Cell>()) as u64; + } + } + bytes += map::map_native_size(obj); bytes += set::set_native_size(obj); bytes += disposable_stack::disposable_stack_native_size(obj); diff --git a/crates/oxide_vm/src/test262_host.rs b/crates/oxide_vm/src/test262_host.rs index dcef9805..88586ae0 100644 --- a/crates/oxide_vm/src/test262_host.rs +++ b/crates/oxide_vm/src/test262_host.rs @@ -86,9 +86,9 @@ pub fn agent(vm: &mut Vm, _args: &[u8]) -> NativeResult { /// 3. `global` 数据属性指向当前 session global;`$262` 本体挂到 global。 /// /// # 注意事项 -/// - 宿主对象经 `Box::into_raw` 持有,随 session 生命周期存活(与 Reflect/Iterator -/// 等全局对象同一约定);每次调用新建对象,重复绑定会泄漏旧对象(全量初始化 -/// 与 full_reset 频率低,可接受)。 +/// - 宿主对象登记进 world 释放表,session 收尾时统一释放(与 Reflect/Iterator +/// 等全局对象同一约定);每次调用新建对象,同一 session 内重复绑定会泄漏旧对象 +/// (全量初始化与 full_reset 频率低,可接受)。 pub fn bind_test262_host(core: &Arc, session: &KernelSession, global: &mut JsObject) { let object_proto = session.builtin_world().object_proto.as_ptr() as *mut JsObject; let mut host = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); @@ -106,5 +106,7 @@ pub fn bind_test262_host(core: &Arc, session: &KernelSession, global ); let global_this = JsValue::from_js_object(global as *mut JsObject); bind_global_value(core, &mut host, "global", global_this); - bind_global_value(core, global, "$262", JsValue::from_js_object(Box::into_raw(Box::new(host)))); + let host_ptr = Box::into_raw(Box::new(host)); + session.builtin_world().track_leaked_object(host_ptr); + bind_global_value(core, global, "$262", JsValue::from_js_object(host_ptr)); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index dc06234a..f22fa85d 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -555,7 +555,8 @@ pub struct Vm { impl Drop for Vm { fn drop(&mut self) { // 直接 drop(test262 每测试新建即弃)不经 reset/full_reset 路径: - // 统一收尾释放全部 session 堆数据,防逐测试累积泄漏。 + // 统一收尾释放全部 session 堆数据与内建原型属性区,防逐测试累积泄漏。 + self.teardown_intrinsic_protos(); self.teardown_session_heap_data(); } } @@ -974,6 +975,47 @@ impl Vm { self.gc_state.session_bytes_peak } + /// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆 + /// 账目(串/BigInt/cell/属性向量)。单次 run 内单调不减(执行期对象不回收、 + /// 串 GC 只降手工堆账目而 arena 不减);run 边界(reset)后重新起算, + /// 供单 run 分配上限判定。 + /// + /// 注意:手工堆账目只在 promote/字符串分配点更新,执行期对象属性区 + /// (元素/属性向量扩容)增长对其不可见——上限判定须配合 + /// [`Self::run_alloc_bytes_full`] 的深采样层。 + pub(crate) fn run_alloc_bytes(&self) -> usize { + self.epoch.bump().allocated_bytes() + + self.gc_state.session_epoch.allocated_bytes() + + self.gc_state.session_bytes_allocated + } + + /// 本 run 累计分配字节的全量重算版:arena 计数器之外,逐一重算已登记对象 + /// 的堆数据(属性/元素向量 + upvalue 列表 + native 状态盒)、session 串、 + /// BigInt 与 upvalue cell 的容量。采样深度高于账目更新点, + /// 兜住属性区扩容这类账目盲区。 + pub(crate) fn run_alloc_bytes_full(&self) -> u64 { + let mut bytes = self.run_alloc_bytes() as u64; + for &ptr in self + .gc_state + .epoch_object_ptrs + .iter() + .chain(self.gc_state.session_object_ptrs.iter()) + { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自对象表登记,dispatch 安全点处仍有效。 + bytes += SessionGc::object_heap_data_bytes(unsafe { &*ptr }); + } + for &ptr in &self.gc_state.session_string_ptrs { + // SAFETY: ptr 来自字符串表登记,收尾前始终有效。 + bytes += (std::mem::size_of::() + unsafe { (*ptr).len() }) as u64; + } + bytes += (self.gc_state.session_bigint_ptrs.borrow().len() * std::mem::size_of::()) as u64; + bytes += (self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::()) as u64; + bytes + } + /// 无条件执行一次完整 session GC(mark + 移动式 sweep + 串/BigInt 清扫)。 /// /// # 副作用 @@ -1525,9 +1567,10 @@ impl Vm { } pub(crate) fn dispatch(&mut self) -> Result { - // config.max_steps 逐指令只读且循环内不变:提到循环外,免每次经 kernel_core - // Arc 指针追寻读取(热点内唯一的 config 访问)。 + // config.max_steps / max_alloc_bytes 逐指令只读且循环内不变:提到循环外, + // 免每次经 kernel_core Arc 指针追寻读取(热点内仅有的 config 访问)。 let max_steps = self.kernel_core.config.max_steps; + let max_alloc_bytes = self.kernel_core.config.max_alloc_bytes; let mut steps: u64 = 0; loop { steps += 1; @@ -1554,6 +1597,21 @@ impl Vm { return Err(format!("VM step limit exceeded at pc={}", self.pc)); } } + // 单 run 分配上限:账目盲区(属性区扩容)靠两层采样兜住——轻层每 + // 64 指令读三个计数器,深层每 2^18 指令全量重算(含逐对象属性区 + // 重算);超限 run 按步数超限同款处理(默认 skip / --no-skip 下 + // fail),防单测试 arena 高水位拖垮宿主。 + if let Some(cap) = max_alloc_bytes { + let deep = (steps & 0x3FFFF) == 0; + if deep || (steps & 0x3F) == 0 { + let used = if deep { self.run_alloc_bytes_full() as usize } else { self.run_alloc_bytes() }; + if used > cap { + vm_warn!("dispatch: memory limit {cap} exceeded (used {used}) at pc={}", self.pc); + self.profiling.set_instruction_count(steps); + return Err(format!("VM memory limit {cap} exceeded (used {used}) at pc={}", self.pc)); + } + } + } if self.pc >= self.bytecode.len() { let tail: Vec = self .bytecode diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index a8fb82cd..dce0f89f 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -21,6 +21,18 @@ impl Vm { /// 超过阈值后 O(n²) 拷贝成本超过节点开销,Cons 的 O(1) 链接才值得。 pub(crate) const CONS_FLATTEN_BYTES: usize = 128; + /// 替换本 VM 独占的内建 P 对象:先恰好释放旧副本的属性区一次, + /// 再让旧 Arc 归零(旧副本无 Drop 口径兜底,full_reset 重初始化 + /// 路径必须显式释放,否则逐测试累积)。 + pub(crate) fn swap_intrinsic_proto(slot: &mut P, new_obj: JsObject) { + // SAFETY: 旧副本为本 VM 独占(其余引用均为本次替换前克隆), + // 属性区仅此一处释放并置空(幂等)。 + unsafe { + (&mut *slot.as_mut_ptr()).release_raw_heap(); + } + *slot = P::new(new_obj); + } + /// 以最小配置创建独立 VM:新建 `KernelCore` + `KernelSession` 并初始化内置对象。 pub fn new() -> Self { let core = KernelCore::new(KernelConfig::minimal()); @@ -339,13 +351,15 @@ impl Vm { self.active_reg_limit = 0; } - /// 释放全部 session 堆数据:epoch 对象堆数据 + session 对象堆数据 + session 串 + - /// BigInt box + upvalue cell box。 + /// 释放全部 session 堆数据:epoch 对象堆数据 + session 对象堆数据 + upvalue + /// 列表 + session 串 + BigInt box + upvalue cell box。 /// /// 供 `full_reset` 与 `Drop` 共用——对象本体(bumpalo arena / epoch bump)由调用方 /// 重置,本函数只释放手工管理的 Box 指针(属性向量、各原生盒、串、BigInt、cell)。 /// 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。 pub(crate) fn teardown_session_heap_data(&mut self) { + // upvalue 列表先于对象表清空释放:去重枚举依赖两份对象表尚存。 + self.free_session_upvalues(); self.free_epoch_object_heap_data(); for ptr in self.gc_state.session_object_ptrs.drain(..) { crate::session_gc::SessionGc::drop_object_heap_data(ptr, true); @@ -355,6 +369,64 @@ impl Vm { self.gc_state.free_cells(); } + /// 集中释放 upvalue 列表(`Box>`):原件与晋升克隆经 + /// `clone_for_session_epoch` 共享同一 Box 分配,逐对象路径释放会双放, + /// 只在此处按指针去重后统一释放。须在两份对象表清空之前调用 + /// (枚举仍存对象完成去重),此时对象已死、Box 无其他读者。 + fn free_session_upvalues(&mut self) { + let mut seen = std::collections::HashSet::new(); + let ptrs = self + .gc_state + .epoch_object_ptrs + .iter() + .chain(self.gc_state.session_object_ptrs.iter()); + for &ptr in ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 epoch/session 对象表登记,收尾时仍指向 arena 内合法对象。 + let up = unsafe { (*ptr).upvalues }; + if up.is_null() || !seen.insert(up) { + continue; + } + // SAFETY: up 由 set_upvalues 的 Box::into_raw 分配,去重保证恰好释放一次。 + unsafe { + drop(Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>)); + (*ptr).upvalues = std::ptr::null_mut(); + } + } + } + + /// 释放 VM 内建原型 P 对象(生成器/Promise/异步族与 Object 原型)的堆外属性区。 + /// + /// # 注意事项 + /// 对象本体随 Arc 引用归零释放(字段 drop);属性区须在此显式释放一次 + /// (`JsObject` 无 Drop 口径)。仅释放本 VM 独占(强引用计数为 1)的副本: + /// `object_prototype` 与 session world 共享 Arc,session 存活期下一测试仍经 + /// world 引用同一对象,其属性区归 session 收尾(`teardown_builtins`)释放。 + pub(crate) fn teardown_intrinsic_protos(&mut self) { + for p in [ + &self.object_prototype, + &self.generator_proto, + &self.generator_function_proto, + &self.promise_constructor, + &self.promise_proto, + &self.aggregate_error_constructor, + &self.aggregate_error_proto, + &self.async_function_proto, + &self.async_generator_proto, + &self.async_generator_function_proto, + ] { + if p.strong_count() != 1 { + continue; + } + // SAFETY: 独占副本的属性区仅此一处释放并置空(幂等)。 + unsafe { + (&mut *p.as_mut_ptr()).release_raw_heap(); + } + } + } + fn free_epoch_object_heap_data(&mut self) { let mut freed = 0u64; for ptr in self.gc_state.epoch_object_ptrs.drain(..) { @@ -417,6 +489,7 @@ impl Vm { self.bytecode = Arc::default(); self.immutables_cache.clear(); self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); + self.free_epoch_dead_upvalues(); self.free_epoch_object_heap_data(); self.epoch.reset(); self.gc_state.epoch_object_ptrs.clear(); @@ -424,6 +497,41 @@ impl Vm { self.active_reg_limit = 0; } + /// 释放随本次 epoch 重置死亡的 epoch 对象的 upvalue 列表。 + /// + /// 原件与晋升克隆共享同一 Box 分配:共享项归 session 克隆持有, + /// 留待收尾(`free_session_upvalues`)统一释放,此处只放独占项。 + fn free_epoch_dead_upvalues(&mut self) { + let mut shared: std::collections::HashSet = std::collections::HashSet::new(); + for &ptr in self.gc_state.session_object_ptrs.iter() { + if ptr.is_null() { + continue; + } + // SAFETY: session 对象表此刻未清空,指向 session arena 内合法对象。 + let up = unsafe { (*ptr).upvalues } as usize; + if up != 0 { + shared.insert(up); + } + } + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + for &ptr in self.gc_state.epoch_object_ptrs.iter() { + if ptr.is_null() { + continue; + } + // SAFETY: epoch 对象表此刻未清空,指向 epoch arena 内合法对象。 + let up = unsafe { (*ptr).upvalues } as usize; + if up == 0 || shared.contains(&up) || !seen.insert(up) { + continue; + } + // SAFETY: up 由 set_upvalues 的 Box::into_raw 分配,去重与共享集 + // 保证本路径恰好释放一次。 + unsafe { + drop(Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>)); + (*ptr).upvalues = std::ptr::null_mut(); + } + } + } + /// 分配一个可被 session GC 回收的字符串 `JsValue`(session-heap 字符串)。 pub fn new_string(&mut self, s: &str) -> JsValue { self.new_string_owned(s.to_string()) @@ -721,6 +829,34 @@ mod tests { Vm::with_kernel_core(KernelCore::new(cfg)) } + /// 单 run 分配上限拦截失控分配:死循环持续 push 的 run 须在触达步数上限前 + /// 以 memory limit 错误终止。 + #[test] + fn run_alloc_cap_stops_runaway_allocation() { + let mut cfg = KernelConfig::minimal(); + cfg.max_alloc_bytes = Some(256 * 1024); + let mut vm = Vm::with_kernel_core(KernelCore::new(cfg)); + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, "var a = []; while (true) { a.push(1); }").expect("parse failed"); + let module = oxide_compiler::compiler::Compiler::new() + .compile(&program) + .expect("compile failed"); + let err = vm.run(&module).expect_err("runaway allocation must hit the alloc cap"); + assert!(err.contains("memory limit"), "unexpected error: {err}"); + assert!(!err.contains("step limit"), "cap 应先于步数上限生效: {err}"); + } + + /// 上限不误伤正常规模分配:同配置下 1 万元素数组构造正常完成。 + #[test] + fn run_alloc_cap_allows_normal_allocation() { + let mut cfg = KernelConfig::minimal(); + cfg.max_alloc_bytes = Some(4 * 1024 * 1024); + let mut vm = Vm::with_kernel_core(KernelCore::new(cfg)); + let result = run_source(&mut vm, "var a = []; for (var i = 0; i < 10000; i++) { a.push(i); } a.length"); + assert!(result.is_int(), "expected int length, got {result:?}"); + assert_eq!(result.as_int(), 10000); + } + /// 直接恢复生成器一步:等价 `it.next()`,但避免跨 run(sub_modules 重建会使 /// 挂起帧失效)——回归聚焦 GC 搬移后的状态盒有效性。 fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue {