fix: 写屏障退直通并按趟修复 global 的 epoch 子引用

This commit is contained in:
2026-09-20 06:30:54 +00:00
parent 2e6c83576b
commit 4285086b0e
3 changed files with 286 additions and 43 deletions
+34 -39
View File
@@ -9,14 +9,9 @@ use crate::vm::Vm;
use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array};
impl Vm {
pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool {
if target_ptr.is_null() {
return false;
}
let global_ptr = self.session.global_object().as_ptr() as *mut JsObject;
std::ptr::eq(target_ptr, global_ptr) || unsafe { (&*target_ptr).is_session_epoch() }
}
/// 单对象晋升测试入口:取/清共享转发表后调 `promote_object_inner`。
/// 生产路径(边界修复/执行期晋升档/根晋升)各自持表管理,不经过此包装。
#[cfg(test)]
pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject {
vm_debug!("promote_object: src={:p}", src);
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
@@ -135,9 +130,11 @@ impl Vm {
/// 对象的引用克隆进 session(JS 边 + native 状态盒),消除 epoch 重置后的
/// 悬垂指针。
///
/// 覆盖两类绕过写屏障的引用来源:函数对象直接 session 分配后捕获的
/// 覆盖三类绕过写屏障的引用来源:函数对象直接 session 分配后捕获的
/// `captured_this`/`home_object`/upvalue cell/属性值(SET_HOME_OBJECT 与
/// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值。
/// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值;
/// global 对象的属性值(逃逸写直通后 epoch 值直落 global 槽,global 不入
/// session 对象表,由 `rewrite_session_epoch_refs` 同趟改写)。
/// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/
/// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。
///
@@ -145,11 +142,9 @@ impl Vm {
/// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入):
/// 本方法原地改写 session 对象的引用字段,执行期调用将使 builtin 局部
/// 裸指针失效(与 `collect_session_gc` 同前提)。
/// - session 对象表为空时 global 的 epoch 子引用仍须修复,不做空表早退。
pub fn promote_session_epoch_refs(&mut self) {
let objects = std::mem::take(&mut self.gc_state.session_object_ptrs);
if objects.is_empty() {
return;
}
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
self.rewrite_session_epoch_refs(&objects, &mut forwarding);
forwarding.clear();
@@ -163,6 +158,9 @@ impl Vm {
/// epoch 子对象经转发表克隆进 session(递归去重共享与环),已是 session/
/// 非 epoch 的值原样保留。JS 边与 native 状态盒走同一改写闭包。
///
/// global 对象不入 session 对象表(P 根,不在 arena 内):其 epoch 子引用
/// 与列表同趟改写,无 native 盒只覆盖 JS 边。
///
/// # 注意事项
/// - 调用方持有转发表期间不得让其它晋升路径改动 `gc_state.forwarding`;
/// 晋升新克隆推入的对象表由调用方决定何时拼回。
@@ -218,6 +216,14 @@ impl Vm {
}
}
}
// global 对象不入 session 对象表:其 epoch 子引用(逃逸写直通留存、
// 函数目标豁免、native 盒直插)与列表同趟修复,免 epoch 释放后悬垂。
let global_ptr = self.session.global_object().as_ptr() as *mut JsObject;
// SAFETY: global 归本 session 所有,安全点内指针有效。
unsafe {
let obj = &mut *global_ptr;
obj.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding));
}
}
/// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session,
@@ -257,25 +263,15 @@ impl Vm {
self.gc_state.forwarding = forwarding;
}
/// 逃逸写屏障:写向全局/session 对象的对象值不指向 epoch,否则 epoch
/// 重置后悬垂。session 函数目标豁免晋升——克隆会令写入侧与字节码持有的
/// 原始对象分裂(类构造器在原型上续建方法会落到非克隆体);函数持有的
/// epoch 子引用由 `promote_session_epoch_refs` 在 epoch 边界统一修复。
pub(crate) fn promote_if_needed_for_write_ptr(&mut self, target_ptr: *mut JsObject, value: JsValue) -> JsValue {
if !value.is_object() || !self.is_session_escape_root_ptr(target_ptr) {
return value;
}
let value_ptr = value.as_js_object_ptr();
if value_ptr.is_null() {
return value;
}
// SAFETY: 执行核心产出的对象值,指针在 session 生命周期内有效。
if unsafe { &*target_ptr }.is_function() {
return value;
}
if unsafe { &*value_ptr }.is_epoch() {
return JsValue::from_js_object(self.promote_object(value_ptr));
}
/// 逃逸写屏障:写向全局/session 目标的对象值原样直通。
///
/// session 对象持有 epoch 子引用是 GC 设计的一等存活态:epoch 边界 reset
/// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环),
/// 执行期收集晋升档按同一转发表改写根与存活 session 对象子引用,两条
/// 路径覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针
/// 全程有效。此处若克隆,同一逻辑对象分裂为原件与克隆两份,经克隆的写
/// 对方经原件的读不可见(builtin 帧指针与逃逸目标双引用并存时即暴露)。
pub(crate) fn promote_if_needed_for_write_ptr(&mut self, _target_ptr: *mut JsObject, value: JsValue) -> JsValue {
value
}
}
@@ -407,20 +403,19 @@ mod tests {
}
#[test]
fn session_arena_barrier_promotes_global_root_write() {
fn session_arena_barrier_global_root_write_returns_original() {
let mut vm = Vm::new();
let value = JsValue::from_js_object(plain_object(&mut vm));
let global_ptr = vm.session.global_object().as_ptr() as *mut JsObject;
let promoted = vm.promote_if_needed_for_write_ptr(global_ptr, value);
assert!(promoted.is_object());
assert!(!is_epoch_object(&vm, promoted));
assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() });
assert_eq!(promoted, value);
assert!(is_epoch_object(&vm, promoted), "epoch 子引用应直通留存,由边界/晋升档统一修复");
}
#[test]
fn session_arena_barrier_promotes_already_session_target_write() {
fn session_arena_barrier_session_target_write_returns_original() {
let mut vm = Vm::new();
let target_epoch = plain_object(&mut vm);
let target = vm.promote_object(target_epoch);
@@ -428,8 +423,8 @@ mod tests {
let promoted = vm.promote_if_needed_for_write_ptr(target, value);
assert!(!is_epoch_object(&vm, promoted));
assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() });
assert_eq!(promoted, value);
assert!(is_epoch_object(&vm, promoted), "session 目标上的 epoch 值应直通留存");
}
#[test]
+10 -4
View File
@@ -58,15 +58,21 @@ fn sweep_runner_forges_rejects_live_vm() {
#[test]
fn full_reset_with_session_objects_forces_global_rebuild() {
let mut vm = Vm::new();
// 池路径场景:`globalThis.Array = {}` 覆盖既有 global 槽(不递增 generation),
// 新值 `{}` 经 promote 进入 session——global 保留时该指针将悬垂。
// 池路径场景:`globalThis.Array = {}` 覆盖既有 global 槽——写屏障直通令
// 新值 `{}` 留存 epoch,global 持 epoch 子引用;global 保留时该指针将悬垂。
// 覆盖写推进 global generation,快照对比发现脏 global,full_reset 必须重建。
let _ = run_source(&mut vm, "globalThis.Array = {}; 0");
assert!(!vm.gc_state.session_object_ptrs.is_empty(), "覆盖写应触发 promote 进入 session");
let written = global_prop(&vm, "Array");
// SAFETY: 覆盖值是本 VM 自有的 epoch 对象,本 session 内指针有效。
assert!(
unsafe { (&*written.as_js_object_ptr()).is_epoch() },
"覆盖值应直通留存 epoch,不克隆进 session"
);
let old_global = vm.session.global_object.as_ptr();
vm.full_reset();
// global 必须重建:旧 global 与其 session 对象随 epoch 释放,Array 恢复内置构造器。
// global 必须重建:旧 global 与其 epoch 子引用随 epoch 释放,Array 恢复内置构造器。
assert!(!std::ptr::eq(old_global, vm.session.global_object.as_ptr()));
assert!(std::ptr::eq(
global_prop(&vm, "Array").as_js_object_ptr(),
@@ -0,0 +1,242 @@
//! 逃逸写屏障 identity 分裂判别钉(写屏障退直通后:原件与逃逸目标恒同一对象)。
//!
//! 覆盖形态:IIFE 纯 JS 逃逸写、flat/flatMap/splice/for-of/嵌套元素窗内访问器
//! 逃逸写、setter 深度、晋升压载下双引用一致性;对照钉钉住 native 盒双态一致
//! 与跨 reset 边界克隆收敛。期望值与 node 行为一致。
//!
//! 关键不变式:逃逸写(对象值写向全局/session 目标)之后,原件引用与逃逸目标
//! 引用指向同一对象,经任一路径的读写互见;执行期晋升与边界修复不引入第二份。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_kernel::kernel::{KernelConfig, KernelCore};
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")
}
fn vm_default() -> Vm {
Vm::new()
}
/// 低 session GC 阈值 VM:执行期两档收集在少量分配内即可触发。
fn vm_with_threshold(bytes: usize) -> Vm {
let mut config = KernelConfig::minimal();
config.set_session_gc_threshold(bytes);
Vm::with_kernel_core(KernelCore::new(config))
}
/// 执行源码并取字符串完成值。
fn eval_string(vm: &mut Vm, source: &str) -> String {
let result = vm.run(&Arc::new(compile(source))).expect("run");
vm.lookup_str(result).expect("完成值应为字符串").to_string()
}
/// P1 最基础形态:IIFE 内 `g = inner; g[1] = 99`(无 builtin 无 getter),
/// 逃逸写后原件、逃逸目标值与 identity 三读一致。
#[test]
fn escape_write_iife_global_no_accessor() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
g = inner; \
g[1] = 99; \
return inner[1] + '|' + g[1] + '|' + (inner === g); \
})()",
);
assert_eq!(out, "99|99|true", "逃逸写后原件与逃逸目标应同一对象且值互见");
}
/// P2 flat 主形态:getter 内逃逸写后,flat 续读应见写入值而非原件旧值。
#[test]
fn escape_write_flat_accessor_readback() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \
return JSON.stringify(inner.flat()); \
})()",
);
assert_eq!(out, "[1,99]", "flat 经原件续读应见 getter 内逃逸写值");
}
/// P3 splice 移位 SET 形态:getter 逃逸写后,移位写经 setter 落原件,
/// 读回链(removed 值、移位后空洞、setter 捕获值)三者与 node 一致。
#[test]
fn escape_write_splice_shifted_readback() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
var store = 1; \
Object.defineProperty(inner, 0, { \
configurable: true, \
get: function(){ g = inner; g[1] = 99; return store; }, \
set: function(v){ store = v; } \
}); \
var r = inner.splice(0, 1); \
return JSON.stringify(r) + '|' + inner[1] + '|' + store; \
})()",
);
assert_eq!(out, "[1]|undefined|99", "移位 SET 应经原件 setter 落值");
}
/// P4 setter 深度:`g[1] = 99` 经 setter 写 this 命名属性,原件与逃逸目标
/// 均应见 setter 写入。
#[test]
fn escape_write_setter_depth_original_visible() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \
Object.defineProperty(inner, 1, { configurable: true, set: function(v){ this._t = v; } }); \
inner.flat(); \
return (inner._t === 99) + '|' + (g._t === 99); \
})()",
);
assert_eq!(out, "true|true", "setter 写入应落在原件上且双引用互见");
}
/// P5 identity 面:逃逸写后大量分配触发执行期晋升,双引用经晋升档
/// 改写后仍同一对象(晋升不引入克隆分裂)。
#[test]
fn escape_write_identity_after_in_run_promotion() {
let mut vm = vm_with_threshold(4096);
let out = eval_string(
&mut vm,
"var inner = [1, 2]; \
g = inner; \
var big = []; \
for (var i = 0; i < 50000; i++) big.push({ v: i }); \
(inner === g) + '|' + inner[1] + '|' + g[1]",
);
assert_eq!(out, "true|2|2", "执行期晋升后双引用应仍同一对象且值一致");
assert!(vm.session_gc_stats().total_collections > 0, "应触发执行期收集");
}
/// P5 值面对照钉:晋升压载下双引用读回值恒一致(压载只作收集触发器)。
#[test]
fn escape_write_value_consistent_under_in_run_pressure() {
let mut vm = vm_with_threshold(4096);
let out = eval_string(
&mut vm,
"var inner = [1, 2]; \
g = inner; \
var big = []; \
for (var i = 0; i < 50000; i++) big.push({ v: i }); \
inner[1] + '|' + g[1]",
);
assert_eq!(out, "2|2", "压载收集后双引用读回值应一致");
assert!(vm.session_gc_stats().total_collections > 0, "应触发执行期收集");
}
/// P6 for-of 形态:被迭代数组的访问器逃逸写后,迭代续读与迭代后读回一致。
#[test]
fn escape_write_for_of_iterated_readback() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \
var s = 0; \
for (var x of inner) s += x; \
return s + '|' + inner[1]; \
})()",
);
assert_eq!(out, "100|99", "迭代续读应见逃逸写值");
}
/// P7 对照钉:Map native 盒直插(键值不经过写屏障),盒内值与原件双态
/// 恒一致——撤屏障克隆后该面不变。
#[test]
fn map_native_box_dual_state_consistent() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
var m = new Map(); \
m.set('k', inner); \
var h = m.get('k'); \
h[1] = 99; \
return inner[1] + '|' + h[1] + '|' + (inner === h); \
})()",
);
assert_eq!(out, "99|99|true", "native 盒读回与原件应双态一致");
}
/// P8 对照钉:跨 run 边界(reset 路径)session 对象的 epoch 子引用经边界
/// 修复克隆收敛——双引用跨边界恒同一对象(REPL 两轮语义的 test 形态)。
#[test]
fn cross_reset_boundary_clone_converges_identity() {
let mut vm = vm_default();
vm.run(&Arc::new(compile("globalThis.inner = [1, 2]; globalThis.g = globalThis.inner; 0")))
.expect("run1");
vm.reset();
let out = eval_string(
&mut vm,
"globalThis.g[1] = 99; (globalThis.inner[1] === 99) + '|' + (globalThis.inner === globalThis.g)",
);
assert_eq!(out, "true|true", "跨 reset 边界后双引用应同一对象且值互见");
}
/// P9 嵌套元素窗:getter 逃逸写嵌套数组后,flat 递归展开经双路读回一致。
#[test]
fn escape_write_nested_element_window() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var nested = [7, 8]; \
var inner = [nested]; \
Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = nested; g[1] = 99; return g; } }); \
var r = inner.flat(); \
return JSON.stringify(r) + '|' + nested[1]; \
})()",
);
assert_eq!(out, "[7,99]|99", "嵌套元素窗读回应见逃逸写值");
}
/// P10 flatMap 形态:回调结果展开 + 嵌套元素 Get 双窗,逃逸写后经原件读回。
#[test]
fn escape_write_flat_map_accessor_readback() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
Object.defineProperty(inner, 0, { configurable: true, get: function(){ g = inner; g[1] = 99; return 1; } }); \
return JSON.stringify(inner.flatMap(function(v){ return [v]; })); \
})()",
);
assert_eq!(out, "[1,99]", "flatMap 续读应见 getter 内逃逸写值");
}
/// P11 对照钉:concat arraylike 无访问器纯赋值窗(length/元素 Get 面),
/// 无逃逸写时结果与 node 逐字一致。
#[test]
fn concat_arraylike_no_accessor_control() {
let mut vm = vm_default();
let out = eval_string(
&mut vm,
"(function(){ \
var inner = [1, 2]; \
var o = { length: 2, 0: 1, 1: 2 }; \
return JSON.stringify(inner.concat(o)); \
})()",
);
assert_eq!(out, "[1,2,{\"0\":1,\"1\":2,\"length\":2}]", "无访问器窗行为应对照 node");
}