fix: try 内 return 泄漏 TryHandler,异常跳回死函数 catch 死循环

This commit is contained in:
爱特每羊队
2026-08-09 07:41:34 +08:00
parent 2f8f1c3b71
commit d4fffcdcc6
6 changed files with 143 additions and 0 deletions
+28
View File
@@ -214,6 +214,12 @@ impl Vm {
self.frames.len(),
self.frames.last().map(|f| f.return_addr).unwrap_or(0)
);
// return 逃出整个函数:当前帧残留的纯 catch handler 一律弹出(return 不被
// catch 捕获),finally handler 保留给下方完成穿越逐个执行。即使 emit 侧已
// 从栈顶弹出连续 catch,这里仍扫描兜底——finally 之下的 catch 无法由
// TRY_END 直接弹出。防 handler 泄漏到已返回函数,导致后续异常 unwind
// 跳回死函数的 catch 形成死循环。
self.pop_frame_catch_handlers();
let crossed = self
.try_stack
.iter()
@@ -228,9 +234,31 @@ impl Vm {
self.do_return(result)
}
/// 弹出当前帧内所有残留的纯 catch handler(无 finally 域),供 return 逃出
/// 本函数时清理。保留 finally handler(供完成穿越)与其它帧的 handler。
///
/// # 边界与前提
/// - 只处理 `frame_depth == frames.len()` 的 handler:return 只逃出本帧,
/// 调用者的 handler 必须保留。
/// - 纯 catch 判定为 `finally_pc.is_none()`(不携带 finally 的 TRY_BEGIN)。
fn pop_frame_catch_handlers(&mut self) {
let depth = self.frames.len();
let mut kept = Vec::with_capacity(self.try_stack.len());
for h in self.try_stack.drain(..) {
if h.frame_depth == depth && h.finally_pc.is_none() {
continue;
}
kept.push(h);
}
self.try_stack = kept;
}
/// 实际执行返回:弹出当前帧并交付返回值(供 dispatch_return 与 finally 完成
/// 恢复共用)。
fn do_return(&mut self, result: JsValue) -> Result<Option<JsValue>, String> {
// 兜底:return 完成恢复路径(dispatch_try_finally_end 直达)也可能携带
// 未清理的纯 catch handler,先弹出再弹帧。
self.pop_frame_catch_handlers();
if let Some(frame) = self.frames.pop() {
self.cell_stack.pop();
let construct_result_reg = frame.construct_result_reg;
+7
View File
@@ -227,6 +227,13 @@ impl Vm {
pub(crate) fn unwind(&mut self) -> Result<(), String> {
vm_debug!("unwind: {} try handlers on stack", self.try_stack.len());
while let Some(mut handler) = self.try_stack.pop() {
if handler.frame_depth > self.frames.len() {
// 残留 handler 所属帧已返回(return 逃出泄漏的兜底):丢弃防跳回
// 死函数的 catch/finally——否则 catch 再抛会反复弹出同一 handler
// 形成死循环。其 for_of_depth 快照属于已死帧,也不可据此关闭
// 当前帧的迭代器,故整体跳过。
continue;
}
while self.frames.len() > handler.frame_depth {
if let Some(frame) = self.frames.pop() {
self.cell_stack.pop();
@@ -189,3 +189,60 @@ fn throw_in_finally_discards_pending_break() {
"9"
);
}
#[test]
fn return_in_try_catch_cleans_handler() {
assert_eq!(eval("function f(){ try{ return 1 } catch(e){ return 2 } }; f()"), "1");
}
#[test]
fn return_in_try_catch_value_path() {
assert_eq!(eval("function f(){ try{ throw 1 } catch(e){ return e } }; f()"), "1");
}
#[test]
fn return_in_try_finally_runs_finally() {
assert_eq!(eval("var fin = 0; function f(){ try{ return 1 } finally{ fin = 1 } } f() * 10 + fin"), "11");
}
#[test]
fn finally_throw_overrides_return() {
assert_eq!(eval("function f(){ try{ return 1 } finally { throw 2 } } ; try{ f() }catch(e){ e }"), "2");
}
#[test]
fn rethrow_in_leaked_catch_no_longer_hangs() {
// return 穿过 try/catch 后,泄漏的 catch handler 不得在后续异常时被跳入:
// 否则 catch 再 throw → 同一 handler 再弹 → 无限循环。修复后该 handler
// 在 return 时被清理 / unwind 时被跳过,异常交给外层 catch。
assert_eq!(
eval("var n = 0; function f(){ try { return 1 } catch(e) { throw e; } } f(); try { undefined.x } catch(e){ n = 1; } n"),
"1"
);
}
#[test]
fn return_crosses_finally_below_outer_catch() {
// return 穿过 finally(其下还有外层 catch):finally 必须执行,外层 catch
// 不得残留泄漏(该场景 TRY_END 无法从栈顶弹出,靠运行时扫描兜底)。
assert_eq!(
eval("var fin = 0; function f(){ try { try { return 1 } finally { fin = 1 } } catch(e){ return 2 } } f() * 10 + fin"),
"11"
);
}
#[test]
fn return_crosses_outer_finally_above_inner_catch() {
assert_eq!(
eval("var fin = 0; function f(){ try { try { return 1 } catch(e){ return 2 } } finally { fin = 1 } } f() * 10 + fin"),
"11"
);
}
#[test]
fn leaked_nested_handler_then_exception_is_caught() {
assert_eq!(
eval("function f(){ try { try { return 1 } finally { } } catch(e){ } } f(); var n = 0; try { undefined.x } catch(e){ n = (e.name === 'TypeError') ? 1 : 2 } n"),
"1"
);
}