fix: JS 重入边界 hop 计数每 64 跳强制顶层分配上限采样——堵 native 泵送小重入双上限结构性永不采样盲区

This commit is contained in:
2026-09-11 16:29:38 +00:00
parent f3533167b5
commit 4095ad2c1d
2 changed files with 50 additions and 0 deletions
+24
View File
@@ -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)
+26
View File
@@ -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 {