mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: 写屏障退直通并按趟修复 global 的 epoch 子引用
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user