fix: matchAll 空匹配推进至少一个字符,RegExp 补 GC 搬移深拷贝
matchAll 迭代器在空匹配(range.start == range.end)后把游标停在原位置, 同一位置反复空匹配导致死循环;改为按 UTF-8 字符边界推进(与 regress find_iter 的 next_right_pos 一致),串尾空匹配把游标推到串长之后保证下轮 耗尽。 另修 RegExp 在 GC sweep 与 epoch 晋升中的指针别名:克隆只复制 native_fn 槽 的 Box 指针,源对象释放后克隆持悬垂指针,teardown 双重释放(heap 损坏)。 新增 clone_regexp_native 深拷贝已编译正则,sweep/promote 两处挂接,与 drop 配对保证单次释放。
This commit is contained in:
@@ -146,6 +146,26 @@ pub fn drop_regexp_native(obj: &mut JsObject) -> u64 {
|
|||||||
std::mem::size_of::<regress::Regex>() as u64
|
std::mem::size_of::<regress::Regex>() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 深拷贝已编译正则到新对象(GC 搬移 / 晋升流程用)。
|
||||||
|
///
|
||||||
|
/// 与 drop 配对:新对象获得独立的 `Box<regress::Regex>`,源对象保留自己的指针,
|
||||||
|
/// 两侧各自释放恰好一次,杜绝跨 arena 克隆后的指针别名双释放。
|
||||||
|
pub fn clone_regexp_native(old_obj: &JsObject, new_obj: &mut JsObject) {
|
||||||
|
if !old_obj.is_regexp_obj() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(ptr) = old_obj.native_fn() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let regex_ptr = ptr.as_ptr() as *const regress::Regex;
|
||||||
|
if regex_ptr.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: old_obj 的 native_fn 槽存 `Box<regress::Regex>` 指针,对象存活期间有效。
|
||||||
|
let cloned_ptr = Box::into_raw(Box::new(unsafe { (&*regex_ptr).clone() }));
|
||||||
|
new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned_ptr as *const ()) }));
|
||||||
|
}
|
||||||
|
|
||||||
/// `RegExp.prototype.test(string)`:判断是否匹配。global 模式下从 lastIndex 开始匹配。
|
/// `RegExp.prototype.test(string)`:判断是否匹配。global 模式下从 lastIndex 开始匹配。
|
||||||
pub fn regexp_test<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn regexp_test<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let re_ptr = match get_regexp_ptr(vm, args) {
|
let re_ptr = match get_regexp_ptr(vm, args) {
|
||||||
|
|||||||
@@ -1613,7 +1613,18 @@ pub fn string_match_all_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
|||||||
None => parts.push(String::new()),
|
None => parts.push(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
idx = range.end;
|
// 空匹配(range.start == range.end)必须推进至少一个 UTF-8 字符,否则同一
|
||||||
|
// 位置反复空匹配死循环;空匹配落在串尾时把游标推到串尾之后,保证下一轮
|
||||||
|
// 直接耗尽(与 regress find_iter 的 next_right_pos 推进语义一致)。
|
||||||
|
idx = if range.end > range.start {
|
||||||
|
range.end
|
||||||
|
} else {
|
||||||
|
range.end
|
||||||
|
+ input_str
|
||||||
|
.get(range.end..)
|
||||||
|
.and_then(|s| s.chars().next())
|
||||||
|
.map_or(1, char::len_utf8)
|
||||||
|
};
|
||||||
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32));
|
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32));
|
||||||
let arr_val = make_string_array(vm, parts);
|
let arr_val = make_string_array(vm, parts);
|
||||||
make_match_done_result(vm, arr_val)
|
make_match_done_result(vm, arr_val)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use oxide_types::value::JsValue;
|
|||||||
use rustc_hash::FxBuildHasher;
|
use rustc_hash::FxBuildHasher;
|
||||||
|
|
||||||
use crate::vm::Vm;
|
use crate::vm::Vm;
|
||||||
use oxide_builtins::{array_buffer, data_view, map, set, typed_array};
|
use oxide_builtins::{array_buffer, data_view, map, regexp, set, typed_array};
|
||||||
|
|
||||||
impl Vm {
|
impl Vm {
|
||||||
pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool {
|
pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool {
|
||||||
@@ -88,6 +88,9 @@ impl Vm {
|
|||||||
crate::async_generator::clone_async_generator_native_with_rewrite(src_ref, dst_ref, |value| {
|
crate::async_generator::clone_async_generator_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||||
self.promote_value_if_epoch_object(value, forwarding)
|
self.promote_value_if_epoch_object(value, forwarding)
|
||||||
});
|
});
|
||||||
|
} else if src_ref.is_regexp_obj() {
|
||||||
|
// 已编译正则是 Box 深拷贝到新对象:源盒随 epoch 释放,互不共享。
|
||||||
|
regexp::clone_regexp_native(src_ref, dst_ref);
|
||||||
}
|
}
|
||||||
dst
|
dst
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,6 +436,9 @@ impl SessionGc {
|
|||||||
crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||||
} else if old_ref.is_generator_obj() {
|
} else if old_ref.is_generator_obj() {
|
||||||
crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||||
|
} else if old_ref.is_regexp_obj() {
|
||||||
|
// 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。
|
||||||
|
regexp::clone_regexp_native(old_ref, new_ref);
|
||||||
}
|
}
|
||||||
forwarding.insert(old_ptr, new_ptr);
|
forwarding.insert(old_ptr, new_ptr);
|
||||||
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
|
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
|
||||||
|
|||||||
@@ -13,6 +13,23 @@ fn to_str(vm: &Vm, val: JsValue) -> String {
|
|||||||
vm.lookup_str(val).unwrap_or_default()
|
vm.lookup_str(val).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regexp_survives_gc_and_still_matches() {
|
||||||
|
// 回归:正则存入全局对象触发晋升/GC 搬移后,native_fn 槽的已编译 Box
|
||||||
|
// 必须深拷贝到新对象(而非共享指针),否则 epoch 释放与 teardown 双重释放。
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "var r = /ab+c/g; globalThis.r = r; r.test('xxabbcx')").unwrap();
|
||||||
|
assert!(result.as_bool());
|
||||||
|
|
||||||
|
// 触发完整收集:存活正则克隆进新 arena,旧 Box 由 sweep 释放。
|
||||||
|
vm.reset();
|
||||||
|
|
||||||
|
let result = eval(&mut vm, "globalThis.r.test('xxabbcx')").unwrap();
|
||||||
|
assert!(result.as_bool(), "GC 后正则应仍可匹配");
|
||||||
|
let result = eval(&mut vm, "globalThis.r.exec('xabbbcx')[0]").unwrap();
|
||||||
|
assert_eq!(to_str(&vm, result), "abbbc");
|
||||||
|
}
|
||||||
|
|
||||||
// --- RegExp constructor ---
|
// --- RegExp constructor ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -696,6 +696,25 @@ fn string_match_all_next_exhausted() {
|
|||||||
assert!(done);
|
assert!(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_match_all_empty_match_advances() {
|
||||||
|
// 回归:空匹配(range.start == range.end)后游标不前进会在同位置反复
|
||||||
|
// 产出空匹配死循环;每位置应恰好一个空匹配后在有限步内耗尽。
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result =
|
||||||
|
eval(&mut vm, "var it = 'abc'.matchAll(/(?:)/g); var c = 0; while (!it.next().done) { c++; } c").unwrap();
|
||||||
|
assert_num_eq(result, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_match_all_star_empty_advances() {
|
||||||
|
// /a*/g 在 'baaab' 上:空匹配(首尾)+ 长匹配混合,须在有限步内耗尽。
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result =
|
||||||
|
eval(&mut vm, "var it = 'baaab'.matchAll(/a*/g); var c = 0; while (!it.next().done) { c++; } c").unwrap();
|
||||||
|
assert_num_eq(result, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
// ── substring 测试 ──
|
// ── substring 测试 ──
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user