From e5d6d1e4d7ba0e150a386cbe533c07f449229bd3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 13:14:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20strings-only=20=E6=94=B6=E9=9B=86?= =?UTF-8?q?=E7=BB=93=E5=B0=BE=E6=B8=85=E5=AF=B9=E8=B1=A1=20mark=20?= =?UTF-8?q?=E4=BD=8D=EF=BC=8C=E6=81=A2=E5=A4=8D=E5=AE=8C=E6=95=B4=E6=94=B6?= =?UTF-8?q?=E9=9B=86=20mark=20=E4=B8=8D=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collect_strings_only 只清位开头、mark 后不还原,残留 marked 对象会让 后续 reset 完整收集的 mark DFS 短路漏标其字符串边,存活串被误释放、 存活对象持悬垂指针。结尾与 sweep 同点 clear_all_marks 收尾,并补 strings-only 后接完整收集的回归(单测 + 集成,覆盖 global 子树串跨 reset 存活)。 --- crates/oxide_vm/src/session_gc.rs | 34 +++++++++++++++++++++++ crates/oxide_vm/tests/gc_runtime_tests.rs | 18 ++++++++++++ 2 files changed, 52 insertions(+) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 37b4ccca..79f39ade 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -564,6 +564,11 @@ impl SessionGc { vm.gc_state.session_bytes_allocated = object_bytes; let freed_bytes = self.sweep_session_strings(vm); + // 恢复 mark 位不变量:mark 之后必由清位收尾(与 sweep 末尾同点清位)。 + // 残留 marked 对象会让下一次完整收集(reset 路径)的 mark DFS 短路漏标, + // 其字符串边不进 live_strings → 存活串被误释放 → 存活对象持悬垂指针。 + self.clear_all_marks(vm); + let elapsed = start.elapsed(); self.total_collections += 1; self.last_collection_duration_us = elapsed.as_micros() as u64; @@ -1357,6 +1362,35 @@ mod tests { assert!(vm.gc_state.session_string_ptrs.contains(&live.as_string_ptr_mut())); } + #[test] + fn strings_only_then_full_collect_keeps_object_strings_live() { + // strings-only 收集后接完整收集(reset 路径):strings-only 残留的 mark 位 + // 不得使完整收集的 mark DFS 短路漏标,存活对象属性中的串须跨收集存活。 + let mut vm = vm_with_threshold(1); + let obj = plain_object(&mut vm); + let s = vm.new_string_owned("kept".repeat(8)); + let s_ptr = s.as_string_ptr_mut(); + unsafe { + (*obj).set_prop_at(0, s); + } + let obj_session = vm.promote_object(obj); + vm.regs[0] = JsValue::from_js_object(obj_session); + + // 第一轮 strings-only:对象被 mark 置位,修复前该位残留至完整收集。 + vm.maybe_collect_session_strings(); + assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr)); + + // 完整收集(reset 的 maybe_collect 路径):对象搬移、字符串地址稳定。 + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); + gc.collect(&mut vm); + vm.gc_state.session_gc = gc; + + // 存活对象经重写仍可达其串:串未被误释放,内容可读。 + assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr)); + assert_eq!(unsafe { (*s_ptr).as_str() }, "kept".repeat(8)); + assert_eq!(unsafe { (*vm.regs[0].as_js_object_ptr()).get_prop_at(0) }, s); + } + #[test] fn strings_only_collection_byte_accounting_matches_survivors() { let mut vm = vm_with_threshold(1); diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 12332948..83329485 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -62,6 +62,24 @@ fn reset_preserves_session_strings_after_runtime_gc() { assert_eq!(text, "xy"); } +/// strings-only 收集后 reset 完整收集:global 子树中 session 对象属性里的串 +/// 跨两次收集存活(strings-only 残留 mark 位不得短路完整收集的 mark DFS)。 +#[test] +fn strings_only_then_reset_keeps_global_subtree_strings() { + let mut vm = vm_with_threshold(4096); + let first = compile( + "globalThis.kept = { s: 'he' + 'llo' }; var t; for (var i = 0; i < 500000; i++) { t = 'x' + i; } 0", + ); + vm.run(&first).expect("run1"); + assert!(vm.session_gc_stats().total_collections > 0, "run1 应触发执行期字符串 GC"); + vm.reset(); + + let second = compile("globalThis.kept.s"); + let result = vm.run(&second).expect("run2"); + let text = vm.lookup_str(result).expect("kept.s 应为字符串").to_string(); + assert_eq!(text, "hello"); +} + /// full_reset 清空 session 内存:执行期回收释放过的死串不与完全重置的 /// 整体释放路径重复释放(无双重释放崩溃),重置后引擎可继续运行。 #[test]