fix: upvalue cell 改独立堆分配消除 GC sweep 后悬垂

upvalue cell 原先分配于 session_epoch bump arena,对象 sweep 替换 arena
后旧 Bump drop,全部闭包 cell 悬垂:后续分配复用其内存导致 upvalue 值
被覆盖(私有字段 brand 检查跨语句误报、release 下可致 SIGSEGV)。

cell 改为 Box::into_raw 独立堆分配并登记进 session_cell_ptrs 追踪表,
与 arena 生命周期解耦:sweep 搬移对象时 cell 指针稳定,只重写
cell.value 中的对象引用;full_reset 统一释放,无 double-free。
This commit is contained in:
2026-08-15 18:50:07 +08:00
parent 3eec7fb7ff
commit 13e9ee3543
5 changed files with 141 additions and 14 deletions
+7 -13
View File
@@ -165,8 +165,7 @@ impl Vm {
if current_cells[cell_idx].is_null() {
// 占位 Cell 保持未初始化(TDZ)直到 MAKE_CELL 置位;
// 若绑定为 var 则其初始化 MAKE_CELL 在函数序言先于任何读取执行。
let cell = self.gc_state.session_epoch.alloc(Cell::new(JsValue::undefined(), false));
current_cells[cell_idx] = cell as *mut Cell;
current_cells[cell_idx] = self.gc_state.alloc_cell(JsValue::undefined(), false);
}
current_cells[cell_idx]
};
@@ -190,8 +189,7 @@ impl Vm {
}
if current[cell_idx].is_null() {
// 无占位 cell:新建。
let cell = self.gc_state.session_epoch.alloc(Cell::new(value, true));
current[cell_idx] = cell as *mut Cell;
current[cell_idx] = self.gc_state.alloc_cell(value, true);
} else {
// 更新占位 cell(CREATE_CLOSURE 已建),使闭包 upvalue 指向的
// cell 值跟随初始化;赋值即解除 TDZ。
@@ -217,8 +215,7 @@ impl Vm {
while current.len() <= cell_idx {
current.push(std::ptr::null_mut());
}
let cell = self.gc_state.session_epoch.alloc(Cell::new(value, true));
current[cell_idx] = cell as *mut Cell;
current[cell_idx] = self.gc_state.alloc_cell(value, true);
Ok(())
}
@@ -231,8 +228,7 @@ impl Vm {
}
if current[cell_idx].is_null() {
let val = self.regs[a];
let cell = self.gc_state.session_epoch.alloc(Cell::new(val, true));
current[cell_idx] = cell as *mut Cell;
current[cell_idx] = self.gc_state.alloc_cell(val, true);
}
let c = unsafe { &*current[cell_idx] };
if !c.is_initialized() {
@@ -311,9 +307,8 @@ impl Vm {
} else {
self.regs[rd]
};
let cell = self.gc_state.session_epoch.alloc(Cell::new(val, true));
upvals[uv_idx] = cell as *mut Cell;
self.regs[rd] = cell.value;
upvals[uv_idx] = self.gc_state.alloc_cell(val, true);
self.regs[rd] = unsafe { (*upvals[uv_idx]).value };
return Ok(());
}
}
@@ -344,8 +339,7 @@ impl Vm {
}
vm_debug!("STORE_UPVALUE len={} wrote existing", upvals.len());
} else {
let cell = self.gc_state.session_epoch.alloc(Cell::new(src_val, true));
upvals[uv_idx] = cell as *mut Cell;
upvals[uv_idx] = self.gc_state.alloc_cell(src_val, true);
}
}
}
+86
View File
@@ -741,8 +741,11 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje
#[cfg(test)]
mod tests {
use oxide_compiler::compiler::Compiler;
use oxide_kernel::kernel::{KernelConfig, KernelCore};
use oxide_parser::Allocator;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
use super::*;
use crate::vm::{CallFrame, FrameContinuation};
@@ -1358,6 +1361,89 @@ mod tests {
Vm::with_kernel_core(core)
}
// ── upvalue cell 独立堆分配:跨对象 sweep 存活 ─────────────────────────
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")
}
fn global_prop_opt(vm: &Vm, name: &str) -> Option<JsValue> {
let global = vm.session.global_object();
let si = vm.kernel_core().perm_interner().intern(name).0;
vm.resolve_property(global, si)
}
/// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换),
/// 修复前 cell 分配于旧 arena、随 Bump drop 悬垂;修复后独立堆分配、
/// 地址稳定,sweep 只重写 cell.value 中的对象引用,值跨搬移保留。
#[test]
fn closure_cell_survives_object_sweep() {
let mut vm = vm_with_threshold(1);
vm.run(&compile("var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; 0"))
.expect("run1");
assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell");
// reset 触发对象 sweep:存活对象搬到新 arena,旧 arena 释放。
vm.reset();
assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集");
// 跨搬移后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。
let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上");
let obj = unsafe { &*inc_val.as_js_object_ptr() };
let cells = obj.upvalues_slice();
assert_eq!(cells.len(), 1, "inc 应捕获 counter 一个 cell");
let cell = unsafe { &*cells[0] };
assert_eq!(cell.value, JsValue::int(0), "sweep 后 cell 值应保留为 counter 初值");
assert!(cell.is_initialized(), "sweep 后 cell 初始化位应保留");
}
/// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell
/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址,
/// cell 指针仍稳定(修复前 cell 悬垂、值被复用覆盖)。
#[test]
fn private_brand_cell_survives_object_sweep() {
let mut vm = vm_with_threshold(1);
vm.run(&compile(
"class C { #x = 0; set(v){ this.#x = v; } get(){ return this.#x; } } \
globalThis.c = new C(); globalThis.c.set(4); 0",
))
.expect("run1");
vm.reset();
assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集");
// 实例 c 的方法 get(类原型上)捕获 @@class_brand cell:值须仍为对象。
let c_val = global_prop_opt(&vm, "c").expect("c 应挂在 global 上");
let c_obj = unsafe { &*c_val.as_js_object_ptr() };
let proto_ptr = c_obj.proto().as_js_object_ptr();
let get_val = vm
.resolve_property(unsafe { &*proto_ptr }, vm.kernel_core().perm_interner().intern("get").0)
.expect("类原型应有 get 方法");
let get_obj = unsafe { &*get_val.as_js_object_ptr() };
let cells = get_obj.upvalues_slice();
assert!(!cells.is_empty(), "get 应捕获 @@class_brand cell");
let brand = unsafe { &*cells[0] }.value;
assert!(brand.is_object(), "sweep 后 brand cell 值应仍为 brand 对象");
}
/// full_reset 统一释放全部 cell box 且可重新分配:追踪表清空(无 double-free),
/// 重置后新闭包正常建立新 cell。
#[test]
fn cells_freed_by_full_reset_and_reallocatable() {
let mut vm = Vm::new();
vm.run(&compile("var x = 1; function f() { return x; } globalThis.f = f; f()"))
.expect("run1");
assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty());
vm.full_reset();
assert!(vm.gc_state.session_cell_ptrs.borrow().is_empty(), "full_reset 应释放全部 cell");
let result = vm.run(&compile("var y = 2; function g() { return y; } g()")).expect("run2");
assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取");
}
#[test]
fn allocation_does_not_trigger_gc_before_safe_point() {
let mut vm = vm_with_threshold(1);
+28 -1
View File
@@ -15,7 +15,7 @@ use rustc_hash::FxBuildHasher;
use crate::session_gc::SessionGc;
use crate::vm::ForInIter;
use oxide_types::object::{JsObject, JsString};
use oxide_types::object::{Cell as UpvalueCell, JsObject, JsString};
use oxide_types::value::JsValue;
/// session arena 与 GC 簿记。
@@ -33,6 +33,10 @@ pub(crate) struct GcState {
/// `convert_immutables` 也能登记新 box。与字符串不同,BigInt 不参与
/// mark/sweep 回收(值量少),只在 full_reset 统一释放。
pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>,
/// upvalue cell(`Box<Cell>`)追踪表。`RefCell` 使 `&self` 的分配入口也能
/// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写
/// `cell.value` 中的对象引用),只在 full_reset 统一释放。
pub(crate) session_cell_ptrs: RefCell<Vec<*mut UpvalueCell>>,
pub(crate) session_bytes_allocated: usize,
/// 执行期字符串 GC 的触发水位:本次收集后的存活字节 + 阈值增量。
/// 仅当账目超过水位才在指令边界触发回收——活串超阈值时不会每指令重复
@@ -45,6 +49,29 @@ impl GcState {
pub(crate) fn track_epoch_object(&mut self, ptr: *mut JsObject) {
self.epoch_object_ptrs.push(ptr);
}
/// 分配一个 upvalue cell:独立堆分配并返回裸指针,脱离 session arena 生命周期。
///
/// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 sweep
/// 搬移不触碰 cell 结构体(地址稳定),只重写 `cell.value` 中的对象引用。
/// `&self` 使调用方可与 `cell_stack` 等其它字段的借用并存(分字段借用)。
pub(crate) fn alloc_cell(&self, value: JsValue, initialized: bool) -> *mut UpvalueCell {
let ptr = Box::into_raw(Box::new(UpvalueCell::new(value, initialized)));
self.session_cell_ptrs.borrow_mut().push(ptr);
ptr
}
/// 释放全部 session 堆 upvalue cell box。仅在完全隔离重置(`full_reset`)时调用,
/// 此时没有存活的 cell_stack / 函数对象 upvalues 会引用它们。
pub(crate) fn free_cells(&mut self) {
for ptr in self.session_cell_ptrs.borrow_mut().drain(..) {
// SAFETY: 每个指针来自 alloc_cell 的 Box::into_raw(Box::new(Cell)),
// 且只在这里恰好释放一次。
unsafe {
drop(Box::from_raw(ptr));
}
}
}
}
/// Symbol 的 intern 状态。
+3
View File
@@ -92,6 +92,7 @@ impl Vm {
session_object_ptrs: Vec::new(),
session_string_ptrs: Vec::new(),
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
string_gc_watermark: gc_threshold,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
@@ -193,6 +194,7 @@ impl Vm {
session_object_ptrs: Vec::new(),
session_string_ptrs: Vec::new(),
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
string_gc_watermark: gc_threshold,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
@@ -300,6 +302,7 @@ impl Vm {
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
self.free_session_string_heap_data();
self.free_session_bigint_heap_data();
self.gc_state.free_cells();
self.symbols.reset();
self.root_reg_limit = 0;
self.active_reg_limit = 0;
+17
View File
@@ -227,3 +227,20 @@ fn full_reset_clears_session_after_runtime_gc() {
let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string();
assert_eq!(text, "ok!");
}
/// full_reset 统一释放执行期分配过的 upvalue cell 无 double-free:重置后引擎
/// 可继续运行并重建新闭包(cell 独立堆分配,随 full_reset 恰好释放一次)。
#[test]
fn closure_cells_freed_by_full_reset_without_double_free() {
let mut vm = vm_with_threshold(1);
let first = compile("var x = 1; function f() { return x; } globalThis.f = f; f()");
let result = vm.run(&first).expect("run1");
assert_eq!(format!("{}", result), "1");
vm.full_reset();
let second = compile("'ok' + '!'");
let result = vm.run(&second).expect("run2");
let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string();
assert_eq!(text, "ok!");
}