From 50674ee4f533f46b35759319cbfd399bda702e73 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 13 Sep 2026 11:05:52 +0000 Subject: [PATCH] =?UTF-8?q?builtins/vm:=20matchAll=20=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E6=AD=A3=E5=88=99=E8=BD=BD=E4=BD=93=E4=B8=93=E5=9E=8B=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=EF=BC=8C=E9=87=8A=E6=94=BE/=E6=B7=B1=E6=8B=B7?= =?UTF-8?q?=E8=B4=9D=E7=BB=8F=20RegExp=20=E5=90=8C=E4=B8=80=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=EF=BC=8C=E6=AD=A2=E9=80=90=E8=B0=83=E7=94=A8=20Box=20?= =?UTF-8?q?=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/oxide_builtins/src/regexp.rs | 13 ++- crates/oxide_builtins/src/string.rs | 3 + crates/oxide_types/src/object.rs | 15 +++ crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/session_arena.rs | 2 +- crates/oxide_vm/src/session_gc.rs | 2 +- .../tests/match_all_native_leak_tests.rs | 92 +++++++++++++++++++ 8 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 crates/oxide_vm/tests/match_all_native_leak_tests.rs diff --git a/Cargo.lock b/Cargo.lock index a896084f..ffb4d511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,6 +1351,7 @@ dependencies = [ "oxide_types", "proptest", "regex", + "regress", "rustc-hash", "ryu", "serde_json", diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 97a9c125..31d53ce6 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -129,9 +129,9 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } -/// 只读核算 RegExp 编译正则字节(不释放)。 +/// 只读核算持有编译正则对象(RegExp / matchAll 载体)的正则字节(不释放)。 pub fn regexp_native_size(obj: &JsObject) -> u64 { - if !obj.is_regexp_obj() { + if !obj.holds_compiled_regex() { return 0; } let Some(ptr) = obj.native_fn() else { @@ -144,7 +144,8 @@ pub fn regexp_native_size(obj: &JsObject) -> u64 { std::mem::size_of::() as u64 } -/// 释放 RegExp 对象 native_fn 槽中编译的 `regress::Regex`,返回释放字节数。 +/// 释放持有编译正则对象(RegExp / matchAll 载体)native_fn 槽中的 +/// `regress::Regex`,返回释放字节数。 pub fn drop_regexp_native(obj: &mut JsObject) -> u64 { let bytes = regexp_native_size(obj); if bytes == 0 { @@ -164,8 +165,9 @@ pub fn drop_regexp_native(obj: &mut JsObject) -> u64 { /// /// 与 drop 配对:新对象获得独立的 `Box`,源对象保留自己的指针, /// 两侧各自释放恰好一次,杜绝跨 arena 克隆后的指针别名双释放。 +/// 作用于全部持编译正则的对象形态(RegExp / matchAll 载体)。 pub fn clone_regexp_native(old_obj: &JsObject, new_obj: &mut JsObject) { - if !old_obj.is_regexp_obj() { + if !old_obj.holds_compiled_regex() { return; } let Some(ptr) = old_obj.native_fn() else { @@ -555,6 +557,9 @@ pub fn regexp_symbol_match_all(vm: &mut H, args: &[u8]) -> NativeResu }; let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let mut stub = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); + // 载体专型标签:native_fn 槽的 Box 经 RegExp 同一守卫释放/深拷贝, + // 避免每次 matchAll 泄漏一个已编译正则。 + stub.type_tag = JsObject::OBJ_TYPE_REGEX_STUB; let raw = Box::into_raw(Box::new(compiled)) as *const u8; stub.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(raw as *const ()) })); JsValue::from_js_object(vm.alloc_object(stub)) diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 548970ef..fb4c3888 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -1507,6 +1507,9 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { }; let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let mut stub = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); + // 载体专型标签:native_fn 槽的 Box 经 RegExp 同一守卫释放/深拷贝, + // 避免每次 matchAll 泄漏一个已编译正则。 + stub.type_tag = oxide_types::object::JsObject::OBJ_TYPE_REGEX_STUB; let boxed = Box::new(compiled); let raw = Box::into_raw(boxed) as *const u8; stub.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(raw as *const ()) })); diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 5930769a..aa9f2d02 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -669,6 +669,10 @@ impl JsObject { pub const OBJ_TYPE_PLAIN_MONTH_DAY: u8 = 26; /// Temporal.PlainYearMonth 对象:年/月/参考日/日历 ID 存于 prop 0-3。 pub const OBJ_TYPE_PLAIN_YEAR_MONTH: u8 = 27; + /// matchAll 迭代器内部载体对象:仅持有编译后的 `regress::Regex`(存于 + /// `native_fn` 槽),无 RegExp 可见属性面;`native_fn` 的释放与深拷贝 + /// 守卫与 RegExp 对象一致。 + pub const OBJ_TYPE_REGEX_STUB: u8 = 28; /// `is_session_epoch` 字段中的 session 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -686,6 +690,17 @@ impl JsObject { pub fn is_regexp_obj(&self) -> bool { self.type_tag == Self::OBJ_TYPE_REGEXP } + /// 是否 matchAll 迭代器内部载体(`native_fn` 槽存编译后的 `regress::Regex`)。 + #[inline] + pub fn is_regex_stub_obj(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_REGEX_STUB + } + /// 是否持有编译正则:`native_fn` 槽存 `Box` 的两种对象形态 + /// (RegExp 对象 / matchAll 载体)——其释放、深拷贝、字节核算守卫统一走此谓词。 + #[inline] + pub fn holds_compiled_regex(&self) -> bool { + matches!(self.type_tag, Self::OBJ_TYPE_REGEXP | Self::OBJ_TYPE_REGEX_STUB) + } /// 是否装箱 Boolean 对象。 #[inline] pub fn is_boolean_obj(&self) -> bool { diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index a4c76c50..61edee49 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -31,6 +31,7 @@ num-traits = "0.2" [dev-dependencies] proptest.workspace = true +regress.workspace = true oxide_ir = { path = "../oxide_ir" } oxide_emit = { path = "../oxide_emit" } criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 9482c877..be29524f 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -96,7 +96,7 @@ impl Vm { crate::async_generator::clone_async_generator_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); - } else if src_ref.is_regexp_obj() { + } else if src_ref.holds_compiled_regex() { // 已编译正则是 Box 深拷贝到新对象:源盒随 epoch 释放,互不共享。 regexp::clone_regexp_native(src_ref, dst_ref); } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index bee06de1..705e83af 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -586,7 +586,7 @@ impl SessionGc { crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_generator_obj() { crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_regexp_obj() { + } else if old_ref.holds_compiled_regex() { // 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。 regexp::clone_regexp_native(old_ref, new_ref); } diff --git a/crates/oxide_vm/tests/match_all_native_leak_tests.rs b/crates/oxide_vm/tests/match_all_native_leak_tests.rs new file mode 100644 index 00000000..ecef4be6 --- /dev/null +++ b/crates/oxide_vm/tests/match_all_native_leak_tests.rs @@ -0,0 +1,92 @@ +//! matchAll 已编译正则载体的生命周期钉:字符串模式 / 非 global 正则两条建载体 +//! 路径的 `native_fn` 槽持有 `Box`,死载体须经 sweep 释放 +//! (释放字节计入收集统计)、存活载体跨完整收集深拷贝后迭代器仍正确推进; +//! 载体无 RegExp 可见属性面(toString 保持普通对象标签)。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; +use regress::Regex; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::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))?; + vm.run(&Arc::new(module)) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.lookup_str(val).unwrap_or_default() +} + +const ROUNDS: u64 = 256; + +/// 字符串模式路径:N 个死载体(包装器 + 载体)经完整收集走 sweep 死分支, +/// 释放字节必须含 N 份编译正则——守卫未命中时该部分恒不计入。 +#[test] +fn string_pattern_match_all_boxes_freed_by_sweep() { + let mut vm = Vm::new(); + eval(&mut vm, "for (let i = 0; i < 256; i++) { 'aaaa'.matchAll('a'); }").unwrap(); + let freed_before = vm.session_gc_stats().total_bytes_freed; + vm.reset(); + let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before; + let min_regex_bytes = ROUNDS * std::mem::size_of::() as u64; + assert!( + freed_delta >= min_regex_bytes, + "sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" + ); +} + +/// 非 global 正则路径:`RegExp.prototype.matchAll` 复制补 g 建载体,同口径断言。 +#[test] +fn non_global_regexp_match_all_boxes_freed_by_sweep() { + let mut vm = Vm::new(); + // 真则常驻(存活根),循环内只产死载体:释放下界唯一来源是载体 Box。 + eval(&mut vm, "var r = /ab/; for (let i = 0; i < 256; i++) { r[Symbol.matchAll]('abab'); }").unwrap(); + let freed_before = vm.session_gc_stats().total_bytes_freed; + vm.reset(); + let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before; + let min_regex_bytes = ROUNDS * std::mem::size_of::() as u64; + assert!( + freed_delta >= min_regex_bytes, + "sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" + ); +} + +/// 存活载体跨完整收集:克隆臂深拷贝 Box 后迭代器仍按原语义耗尽。 +#[test] +fn string_pattern_match_all_iterator_survives_full_collect() { + let mut vm = Vm::new(); + eval(&mut vm, "var it = 'abab'.matchAll('ab');").unwrap(); + vm.reset(); + let result = eval( + &mut vm, + "var out = []; var st = it.next(); while (!st.done) { out.push(String(st.value[0])); st = it.next(); } out.join(',')", + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "ab,ab"); +} + +/// 非 global 正则路径的存活载体同样跨收集保持有效。 +#[test] +fn non_global_regexp_match_all_iterator_survives_full_collect() { + let mut vm = Vm::new(); + eval(&mut vm, "var r = /ab/; var it = r[Symbol.matchAll]('abab');").unwrap(); + vm.reset(); + let result = eval( + &mut vm, + "var out = []; var st = it.next(); while (!st.done) { out.push(String(st.value[0])); st = it.next(); } out.join(',')", + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "ab,ab"); +} + +/// 载体经内部属性可达,但无 RegExp 属性面:toString 保持普通对象标签。 +#[test] +fn match_all_carrier_has_no_regexp_surface() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Object.prototype.toString.call('a'.matchAll('a').__mal_re__)").unwrap(); + assert_eq!(to_str(&vm, result), "[object Object]"); +}