diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index d4d2678f..fbde4bea 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -478,6 +478,11 @@ pub struct Vm { pub(crate) root_reg_limit: u8, pub(crate) active_reg_limit: u8, pub(crate) native_call_depth: usize, + /// JS 重入 hop 计数:native 体内(`native_call_depth > 0`)每次嵌套 dispatch + /// 进入计一跳。每 64 跳强制一次顶层分配上限采样——顶层 dispatch 冻结在 native + /// 体内时 `steps` 不推进、重入 dispatch 又太短到不了循环内采样点,分配上限 + /// 否则结构性永不采样(小重入泵送盲区)。run 边界重置。 + pub(crate) reentry_hops: u64, /// inline 同步调用(`call_bytecode_function_inline`,frames 为空)的实参区位置。 /// frames 非空时 CREATE_ARGUMENTS 优先读当前帧的实参区;此字段只服务内联路径。 pub(crate) inline_args_base: u32, @@ -1587,6 +1592,25 @@ impl Vm { let max_steps = self.kernel_core.config.max_steps; let max_alloc_bytes = self.kernel_core.config.max_alloc_bytes; let mut steps: u64 = 0; + // 小重入泵送盲区:native 终端循环泵送短 JS 重入时,顶层 steps 不推进、 + // 每次重入 dispatch 太短到不了循环内 64 指令采样点,分配上限结构性永不 + // 采样。重入边界按 hop 计数,每 64 跳强制一次顶层轻采样兜底。 + if self.native_call_depth > 0 { + self.reentry_hops += 1; + if self.reentry_hops & 0x3F == 0 { + if let Some(cap) = max_alloc_bytes { + let used = self.run_alloc_bytes(); + if used > cap { + vm_warn!( + "dispatch: memory limit {cap} exceeded at reentry hop {} (used {used}) at pc={}", + self.reentry_hops, + self.pc + ); + return Err(format!("VM memory limit {cap} exceeded (used {used}) at pc={}", self.pc)); + } + } + } + } loop { steps += 1; // 执行期 GC 安全点:仅在顶层 dispatch(native_call_depth == 0) diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 49736b96..2c61ab7a 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -83,6 +83,7 @@ impl Vm { root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, + reentry_hops: 0, inline_args_base: 0, inline_args_count: 0, accessor_frame_target_reg: None, @@ -196,6 +197,7 @@ impl Vm { root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, + reentry_hops: 0, inline_args_base: 0, inline_args_count: 0, accessor_frame_target_reg: None, @@ -473,6 +475,8 @@ impl Vm { self.async_gen_dispatch = false; self.async_gen_suspended = false; self.native_call_depth = 0; + // 重入 hop 计数是执行期状态:分配上限按 run 起算,跨 run/reset 不保留。 + self.reentry_hops = 0; // inline 窗口缓冲池内容为已废弃快照,跨 run/reset 不保留。 self.inline_reg_pool = None; // 微任务队列是执行期状态:跨 run 不保留。 @@ -857,6 +861,28 @@ mod tests { assert_eq!(result.as_int(), 10000); } + /// native 终端循环泵送小 JS 重入:每次重入 dispatch 远短于循环内 64 指令采样点, + /// 顶层 steps 不推进——重入边界每 64 hop 强制采样兜底,失控分配以 memory limit + /// 终止而非无限循环。 + #[test] + fn reentry_pump_hits_alloc_cap() { + let mut cfg = KernelConfig::minimal(); + cfg.max_alloc_bytes = Some(256 * 1024); + let mut vm = Vm::with_kernel_core(KernelCore::new(cfg)); + // 永不 done 的生成器经鸭子对象交给 toArray:native 循环每步泵送两个短重入 + // (闭包调用 + 生成器恢复)并新产一个结果对象,分配无界增长。 + let source = "var g = (function* () { for (var i = 0; ; ++i) { yield i; } })(); \ + var obj = { next: function () { return g.next(); } }; \ + Iterator.from(obj).toArray();"; + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + let module = oxide_compiler::compiler::Compiler::new() + .compile(&program) + .expect("compile failed"); + let err = vm.run(&module).expect_err("reentry pump must hit the alloc cap"); + assert!(err.contains("memory limit"), "unexpected error: {err}"); + } + /// 直接恢复生成器一步:等价 `it.next()`,但避免跨 run(sub_modules 重建会使 /// 挂起帧失效)——回归聚焦 GC 搬移后的状态盒有效性。 fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue {