fix: 生成器状态盒深拷贝消除 sweep/promote 共享所有权 UAF,full_reset 补释放 session 堆数据

GeneratorState 状态盒此前由 epoch 原对象独占释放,promote 与 sweep 克隆
浅拷贝 native_data 共享同一盒,旧对象 drop 即悬垂。新增 clone 深拷贝
(参照 async_generator),promote/sweep 两处接入,恢复执行读新盒;
full_reset 在 session arena 重置前遍历 session 对象释放堆数据,
配合深拷贝形成单所有权。补 sweep/promote/full_reset 三回归与
生成器捕获闭包跨 GC 回归。
This commit is contained in:
2026-08-15 19:02:39 +08:00
parent 4373b88303
commit 9e8ee96491
5 changed files with 149 additions and 3 deletions
+18
View File
@@ -872,6 +872,24 @@ pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(J
state.suspended.rewrite_values(rewrite);
}
/// 深拷贝状态盒到新对象(promote / sweep 用):新对象持独立 Box,源盒可安全释放。
pub(crate) fn clone_generator_native_with_rewrite(
old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue,
) {
let Some(state) = generator_state_mut(old) else {
return;
};
let cloned = GeneratorState {
phase: state.phase,
callee: rewrite(state.callee),
args: state.args.iter().copied().map(&mut rewrite).collect(),
this_value: state.this_value,
result: rewrite(state.result),
suspended: state.suspended.clone_with_rewrite(rewrite),
};
new.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8);
}
/// 释放生成器状态盒(对象被 GC 回收时),返回释放字节数。
pub(crate) fn drop_generator_native(obj: &JsObject) -> u64 {
if !obj.is_generator_obj() {
+2 -2
View File
@@ -66,8 +66,8 @@ impl Vm {
self.promote_value_if_epoch_object(value, forwarding)
});
} else if src_ref.is_generator_obj() {
// 生成器状态盒为堆分配、promote 后与源共享同一盒:就地改写其 JsValue。
crate::generator::rewrite_generator_native(dst_ref, |value| {
// 生成器状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。
crate::generator::clone_generator_native_with_rewrite(src_ref, dst_ref, |value| {
self.promote_value_if_epoch_object(value, forwarding)
});
} else if src_ref.is_promise_obj() {
+2
View File
@@ -434,6 +434,8 @@ impl SessionGc {
crate::async_func::clone_async_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_async_generator_obj() {
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);
}
forwarding.insert(old_ptr, new_ptr);
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
+91 -1
View File
@@ -302,8 +302,18 @@ impl Vm {
self.free_epoch_object_heap_data();
self.epoch.reset();
self.gc_state.epoch_object_ptrs.clear();
// 先释放 session 对象堆数据(属性向量 + 各原生盒),再重置 arena:
// 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。
let mut freed = 0u64;
for ptr in self.gc_state.session_object_ptrs.drain(..) {
freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, true);
}
if freed > 0 {
self.gc_state.session_gc.total_bytes_freed =
self.gc_state.session_gc.total_bytes_freed.saturating_add(freed);
self.gc_state.session_gc.last_collection_bytes_freed = freed;
}
self.gc_state.session_epoch.reset();
self.gc_state.session_object_ptrs.clear();
self.gc_state.session_bytes_allocated = 0;
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
@@ -660,6 +670,86 @@ mod tests {
assert!(!vm.session.is_dirty_since_snapshot());
}
fn vm_with_low_threshold() -> Vm {
let mut cfg = KernelConfig::minimal();
cfg.set_session_gc_threshold(1);
Vm::with_kernel_core(KernelCore::new(cfg))
}
/// 直接恢复生成器一步:等价 `it.next()`,但避免跨 run(sub_modules 重建会使
/// 挂起帧失效)——回归聚焦 GC 搬移后的状态盒有效性。
fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue {
match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) {
Ok(crate::generator::GeneratorStep::Suspended { value }) => value,
Ok(crate::generator::GeneratorStep::Completed { value }) => value,
Ok(other) => panic!("unexpected step: {:?}", match other {
crate::generator::GeneratorStep::Thrown { value } => format!("Thrown({value})"),
crate::generator::GeneratorStep::SuspendedRaw { value } => format!("SuspendedRaw({value})"),
_ => String::new(),
}),
Err(e) => panic!("resume failed: {e}"),
}
}
#[test]
fn generator_survives_object_sweep_and_resumes() {
let mut vm = vm_with_low_threshold();
let _ = run_source(
&mut vm,
"function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0",
);
// 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena,
// 状态盒深拷贝为新 Box(reset 会清空模块表使恢复不可行,走收集入口等价验证)。
vm.maybe_collect_session_gc();
assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集");
// 从 global 取 sweep 重写后的生成器(sub_modules/immutables 未重建,可恢复)。
let it = global_prop(&vm, "it");
assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复第二次 yield");
}
#[test]
fn generator_captured_upvalue_survives_sweep() {
let mut vm = vm_with_low_threshold();
let _ = run_source(
&mut vm,
"var x = 0; function* g(){ x++; yield x; x++; yield x; } globalThis.it = g(); globalThis.it.next(); 0",
);
vm.maybe_collect_session_gc();
assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集");
// 挂起帧 cell_stack 与闭包 upvalues 中的 cell 独立堆分配(地址稳定),
// 恢复后继续读写捕获变量。
let it = global_prop(&vm, "it");
assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复捕获变量读写");
}
#[test]
fn generator_promoted_clone_owns_independent_state_box() {
let mut vm = Vm::new();
// `var it = g(); it.next(); it`:it 为 epoch 生成器对象(未逃逸不 promote)。
let it = run_source(&mut vm, "function* g(){ yield 1; yield 2; } var it = g(); it.next(); it");
assert!(it.is_object());
let epoch_ptr = it.as_js_object_ptr();
let epoch_box = unsafe { (*epoch_ptr).native_data() };
// 手动 promote:克隆应深拷贝状态盒(新 Box),与源盒互不共享。
let promoted = vm.promote_object(epoch_ptr);
assert!(!std::ptr::eq(promoted, epoch_ptr));
let promoted_box = unsafe { (*promoted).native_data() };
assert!(!std::ptr::eq(epoch_box, promoted_box), "promote 应深拷贝生成器状态盒");
// 模拟 full_reset 的 epoch 侧释放:源对象与其状态盒随 epoch 回收,
// 并从追踪表移除登记(克隆的后续回收仍由 VM 统一处理)。
let _ = crate::session_gc::SessionGc::drop_object_heap_data(epoch_ptr, false);
vm.gc_state.epoch_object_ptrs.retain(|&p| !std::ptr::eq(p, epoch_ptr));
// 克隆直接恢复执行:读新盒中的挂起状态,不得悬垂。
assert_eq!(resume_one_step(&mut vm, JsValue::from_js_object(promoted)), JsValue::int(2));
}
#[test]
fn full_reset_clean_keeps_session_objects() {
let mut vm = Vm::new();
@@ -0,0 +1,36 @@
//! 生成器 × session GC 回归:promote 后 full_reset 释放 epoch 原对象与 session 克隆
//! 的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。
//! sweep 变体与闭包捕获变体见 `vm_support.rs` 内部测试(需 crate 内 `resume_generator`
//! 在模块表未重建时恢复执行,integration 层无法触达)。
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn compile(source: &str) -> oxide_bytecode::module::CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
Compiler::new().compile(&program).expect("compile")
}
/// promote + full_reset 变体:global 根持有生成器克隆,full_reset 释放 epoch 原对象
/// 与 session 克隆的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。
#[test]
fn generator_promoted_to_global_survives_full_reset_cleanly() {
let mut vm = Vm::new();
vm.run(&compile("function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0"))
.expect("run1");
assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session");
// global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问
// (修复前 epoch 原对象释放共享状态盒 → 读已释放内存)。
vm.full_reset();
assert_eq!(vm.session_object_count(), 0, "full_reset 应清空 session 对象");
let result = vm.run(&compile("typeof it")).expect("run2");
let text = vm.lookup_str(result).expect("typeof 应返回字符串").to_string();
assert_eq!(text, "undefined");
// 新会话可正常创建并推进生成器。
let ok = vm.run(&compile("function* h(){ yield 9; } var it2 = h(); it2.next().value")).expect("run3");
assert_eq!(format!("{ok}"), "9");
}