diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 9c8b04b7..a0ec935c 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -29,6 +29,7 @@ rustc-hash.workspace = true oxide_compiler = { path = "../oxide_compiler" } proptest.workspace = true oxide_parser = { path = "../oxide_parser" } +oxide_ir = { path = "../oxide_ir" } criterion = { version = "0.5", features = ["html_reports"] } [[bench]] diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index f5a4c3f2..7716c49f 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -391,6 +391,7 @@ impl Vm { function_name, caller_reg_limit, saved_reg_offset, + spill_offset: self.spill_stack.len() as u32, saved_this, saved_new_target, callee: super_ctor, diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 28ed6622..1fec8e6e 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -75,6 +75,44 @@ impl Vm { Ok(false) } + /// SPILL:regs[rd] → spill_stack[帧基址 + slot]。ext 字 = slot u16(D-06)。 + /// 帧基址 = 当前 CallFrame.spill_offset(顶层模块无帧 = 0,D-08)。 + #[inline(always)] + pub(crate) fn dispatch_spill(&mut self, rd: usize) -> Result<(), String> { + let slot = self.bytecode[self.pc] as usize; + self.pc += 1; + if slot > u16::MAX as usize { + return Err(format!("SPILL slot {slot} out of range (max 65535)")); + } + let base = self.frames.last().map(|f| f.spill_offset as usize).unwrap_or(0); + let idx = base + slot; + if idx >= self.spill_stack.len() { + self.spill_stack.resize(idx + 1, JsValue::undefined()); + } + vm_trace!("SPILL r{} -> slot[{}] base={}", rd, slot, base); + self.spill_stack[idx] = self.regs[rd]; + Ok(()) + } + + /// UNSPILL:spill_stack[帧基址 + slot] → regs[rd]。越界读 undefined(防御,D-06 slot 由 RegAlloc 保证在界内)。 + #[inline(always)] + pub(crate) fn dispatch_unspill(&mut self, rd: usize) -> Result<(), String> { + let slot = self.bytecode[self.pc] as usize; + self.pc += 1; + if slot > u16::MAX as usize { + return Err(format!("UNSPILL slot {slot} out of range (max 65535)")); + } + let base = self.frames.last().map(|f| f.spill_offset as usize).unwrap_or(0); + let idx = base + slot; + vm_trace!("UNSPILL r{} <- slot[{}] base={}", rd, slot, base); + self.regs[rd] = if idx < self.spill_stack.len() { + self.spill_stack[idx] + } else { + JsValue::undefined() + }; + Ok(()) + } + #[inline(always)] pub(crate) fn dispatch_new_object(&mut self, rd: usize) { vm_trace!("NEW_OBJECT rd={}", rd); diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 0be8023c..d5a65499 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -506,6 +506,9 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje for value in &mut vm.save_stack { *value = rewrite_forwarded_value(*value, forwarding); } + for value in &mut vm.spill_stack { + *value = rewrite_forwarded_value(*value, forwarding); + } for frame in &mut vm.frames { frame.saved_this = rewrite_forwarded_value(frame.saved_this, forwarding); frame.saved_new_target = rewrite_forwarded_value(frame.saved_new_target, forwarding); @@ -586,6 +589,7 @@ mod tests { function_name: 0, caller_reg_limit: 1, saved_reg_offset: 0, + spill_offset: 0, saved_this: JsValue::from_js_object(this_session), saved_new_target: JsValue::from_js_object(child_session), callee: JsValue::from_js_object(child_session), diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index c6d96d42..bdc97410 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -110,6 +110,8 @@ pub struct CallFrame { pub function_name: u32, pub caller_reg_limit: u8, pub saved_reg_offset: u32, + /// push 时记录 spill_stack 长度(D-08 帧边界,SPILL/UNSPILL 基址)。 + pub spill_offset: u32, pub saved_this: JsValue, pub saved_new_target: JsValue, pub callee: JsValue, @@ -159,6 +161,7 @@ pub(crate) struct InlineSyncState { pub(crate) saved_bytecode_stack: Vec>, pub(crate) saved_immutables_stack: Vec<*const [JsValue]>, pub(crate) save_stack: Vec, + pub(crate) spill_stack: Vec, pub(crate) cell_stack: Vec>, } @@ -191,6 +194,9 @@ pub struct Vm { /// registers (`regs[..caller_reg_limit]`) here at `saved_reg_offset`; restore copies /// them back and truncates. Capacity is retained across calls — zero per-call heap alloc. pub(crate) save_stack: Vec, + /// VM 级 spill 栈(D-08,仿 save_stack 帧边界)。`CallFrame.spill_offset` 定位本帧区, + /// 调用子函数时从边界后分配,restore_frame 截断恢复。 + pub(crate) spill_stack: Vec, pub(crate) try_stack: Vec, pub(crate) exception_value: Option, /// Side-channel carrying the JsValue thrown by a sync call whose error was flattened @@ -385,6 +391,10 @@ impl Vm { for &v in &self.save_stack { f(v); } + // spill 栈是 session GC 根(漏根 → 溢出值被回收 → use-after-free) + for &v in &self.spill_stack { + f(v); + } for cell_vec in &self.cell_stack { for &cell_ptr in cell_vec { if cell_ptr.is_null() { @@ -777,6 +787,7 @@ impl Vm { function_name, caller_reg_limit, saved_reg_offset, + spill_offset: self.spill_stack.len() as u32, saved_this, saved_new_target, callee, @@ -989,6 +1000,19 @@ impl Vm { Err(e) => return Err(e), }, + OpCode::MOV => { + vm_trace!("MOV r{} = r{}", rd, a); + self.regs[rd] = self.regs[a]; + } + + OpCode::SPILL => { + self.dispatch_spill(rd)?; + } + + OpCode::UNSPILL => { + self.dispatch_unspill(rd)?; + } + OpCode::CALL => match self.dispatch_call(rd, a, b) { Ok(true) => continue, Ok(false) => {} @@ -1461,6 +1485,7 @@ mod tests { function_name: 0, caller_reg_limit: 2, saved_reg_offset: 0, + spill_offset: 0, saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), callee: JsValue::undefined(), diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 2634225d..9cdbbaa5 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -130,6 +130,7 @@ impl Vm { .unwrap_or(0), caller_reg_limit, saved_reg_offset, + spill_offset: self.spill_stack.len() as u32, saved_this, saved_new_target, callee: constructor, diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 6f7c8ae5..8fdba64a 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -60,6 +60,7 @@ impl Vm { saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack), saved_immutables_stack: std::mem::take(&mut self.saved_immutables_stack), save_stack: std::mem::take(&mut self.save_stack), + spill_stack: std::mem::take(&mut self.spill_stack), cell_stack: std::mem::take(&mut self.cell_stack), }); @@ -111,6 +112,7 @@ impl Vm { self.saved_bytecode_stack = saved.saved_bytecode_stack; self.saved_immutables_stack = saved.saved_immutables_stack; self.save_stack = saved.save_stack; + self.spill_stack = saved.spill_stack; self.cell_stack = saved.cell_stack; result @@ -136,6 +138,8 @@ impl Vm { let len = frame.caller_reg_limit as usize; self.regs[..len].copy_from_slice(&self.save_stack[offset..offset + len]); self.save_stack.truncate(offset); + // spill 帧边界截断(D-08,仿 save_stack 窗口):子函数 spill 数据在帧恢复后丢弃 + self.spill_stack.truncate(frame.spill_offset as usize); self.regs[254] = frame.saved_this; self.regs[255] = frame.saved_new_target; self.active_reg_limit = frame.caller_reg_limit; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 690ca521..b017fcbd 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -37,6 +37,7 @@ impl Vm { saved_bytecode_stack: Vec::new(), saved_immutables_stack: Vec::new(), save_stack: Vec::new(), + spill_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, last_uncaught_value: None, @@ -100,6 +101,7 @@ impl Vm { saved_bytecode_stack: Vec::new(), saved_immutables_stack: Vec::new(), save_stack: Vec::new(), + spill_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, last_uncaught_value: None, @@ -213,6 +215,7 @@ impl Vm { self.saved_bytecode_stack.clear(); self.saved_immutables_stack.clear(); self.save_stack.clear(); + self.spill_stack.clear(); self.cell_stack.clear(); self.temp_immutables.clear(); self.try_stack.clear(); diff --git a/crates/oxide_vm/tests/spill_stack_tests.rs b/crates/oxide_vm/tests/spill_stack_tests.rs new file mode 100644 index 00000000..26a8b5f9 --- /dev/null +++ b/crates/oxide_vm/tests/spill_stack_tests.rs @@ -0,0 +1,120 @@ +//! VM spill 栈执行语义测试(D-06/D-07/D-08):MOV 复制、SPILL/UNSPILL 往返、 +//! 帧边界(父 spill 跨子调用完好)、嵌套双 spill 交互、UNSPILL 越界防御。 +//! +//! 手工 IR 是唯一可达路径——RegAlloc 未接入,真实 JS 编译产物不会含三指令。 + +use oxide_bytecode::module::Constant; +use oxide_bytecode::opcode::OpCode; +use oxide_ir::inst::Inst; +use oxide_ir::operand::Operand; +use oxide_ir::IRFunction; +use oxide_vm::vm::Vm; + +fn run_ir(ir: IRFunction) -> String { + let module = oxide_ir::lower::lower(&ir).expect("lower failed"); + let mut vm = Vm::new(); + match vm.run(&module) { + Ok(v) => format!("{v}"), + Err(e) => format!("vm error: {e}"), + } +} + +fn int_module(insts: Vec, value: i32, n_registers: u8, nested: Vec) -> IRFunction { + let mut f = IRFunction::new(); + f.insts = insts; + f.constants = vec![Constant::Int(value)]; + f.n_registers = n_registers as u32; + f.nested = nested; + f +} + +/// 测试 1:MOV 寄存器复制语义(D-07)。r3=42 → MOV r5=r3 → HALT 输出 42。 +#[test] +fn mov_copies_register() { + let insts = vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::inst_mov(Operand::Reg(5), Operand::Reg(3)), + Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)), + Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None), + ]; + let out = run_ir(int_module(insts, 42, 6, Vec::new())); + assert_eq!(out, "42"); +} + +/// 测试 2:SPILL→UNSPILL 往返(D-06)。SPILL r3 slot0 → MOV 弄脏 r5 → UNSPILL r5 slot0 → 读回 42。 +#[test] +fn spill_unspill_roundtrip() { + let insts = vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::inst_spill(Operand::Reg(3), 0), + Inst::inst_mov(Operand::Reg(5), Operand::Reg(3)), + Inst::inst_unspill(Operand::Reg(5), 0), + Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)), + Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None), + ]; + let out = run_ir(int_module(insts, 42, 6, Vec::new())); + assert_eq!(out, "42"); +} + +/// 测试 3:帧边界——父函数 SPILL 后 CALL 子函数,子函数返回后父 UNSPILL 仍得 42(D-08)。 +#[test] +fn frame_boundary_parent_spill_survives_call() { + let child = int_module( + vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None), + ], + 99, + 4, + Vec::new(), + ); + let insts = vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::inst_spill(Operand::Reg(3), 0), + Inst::create_closure(Operand::Reg(4), 1), + Inst::call(Operand::Reg(4), Operand::This, Operand::Reg(4), 0), + Inst::inst_unspill(Operand::Reg(5), 0), + Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)), + Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None), + ]; + let out = run_ir(int_module(insts, 42, 6, vec![child])); + assert_eq!(out, "42", "父 spill 数据在子调用后必须完好"); +} + +/// 测试 4:嵌套交互——父 SPILL 42、子函数自身也 SPILL(子帧基址 = 父已用长度 1),父 UNSPILL 仍得 42。 +#[test] +fn nested_call_both_spill() { + let child = int_module( + vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::inst_spill(Operand::Reg(3), 0), + Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None), + ], + 99, + 4, + Vec::new(), + ); + let insts = vec![ + Inst::load_const(Operand::Reg(3), 0), + Inst::inst_spill(Operand::Reg(3), 0), + Inst::create_closure(Operand::Reg(4), 1), + Inst::call(Operand::Reg(4), Operand::This, Operand::Reg(4), 0), + Inst::inst_unspill(Operand::Reg(5), 0), + Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)), + Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None), + ]; + let out = run_ir(int_module(insts, 42, 6, vec![child])); + assert_eq!(out, "42", "子帧 spill 截断后父数据必须完好"); +} + +/// 测试 5:UNSPILL 空栈越界防御——读 undefined 不 panic。 +#[test] +fn unspill_empty_stack_returns_undefined() { + let insts = vec![ + Inst::inst_unspill(Operand::Reg(5), 0), + Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)), + Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None), + ]; + let out = run_ir(int_module(insts, 42, 6, Vec::new())); + assert_eq!(out, "undefined"); +}