From 7e46ecfb768793b2418c5d574e5083f90815e596 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 18:36:35 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20sloppy=20eval=20=E9=A1=B6=E5=B1=82?= =?UTF-8?q?=E5=9D=97=E5=87=BD=E6=95=B0=E5=8F=82=E4=B8=8E=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=AE=9E=E4=BE=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/prepass/declare.rs | 21 +-- crates/oxide_emit/src/program.rs | 7 +- crates/oxide_vm/tests/eval_block_fn_tests.rs | 136 +++++++++++++++++++ 3 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 crates/oxide_vm/tests/eval_block_fn_tests.rs diff --git a/crates/oxide_emit/src/prepass/declare.rs b/crates/oxide_emit/src/prepass/declare.rs index 9419f81d..d0c447af 100644 --- a/crates/oxide_emit/src/prepass/declare.rs +++ b/crates/oxide_emit/src/prepass/declare.rs @@ -175,11 +175,12 @@ impl Emitter { /// - 只处理块内函数声明;switch 不推 scope(其 case 内函数声明随 switch /// 作用域修复一并处理);export 声明不可能出现在块内。 /// - `if_arm` 标记当前递归是否位于 `if` 支臂隐式块内。Annex B 下该隐式块的 - /// 函数声明名已有外层 var 绑定承载(非 eval 脚本顶层、未被形参/词法同名 - /// 抑制、且非全局只读三常量)时不建当前块绑定,声明点物化进该 var 槽 - /// (建当前块 Let 会遮蔽同名 var);名无外层 var 绑定(形参/词法抑制、eval - /// 脚本顶层或全局只读三常量)时保留块级绑定承载支臂闭包,避免声明点覆写 - /// 外层绑定。门控只作用于 `if` 支臂下的叶子,块直接子不受影响。 + /// 函数声明名已有外层 var 绑定承载(非 eval 脚本顶层、或 sloppy eval 顶层; + /// 未被形参/词法同名抑制,且非全局只读三常量)时不建当前块绑定,声明点 + /// 物化进该 var 槽(建当前块 Let 会遮蔽同名 var);名无外层 var 绑定 + /// (形参/词法抑制、strict eval 顶层或全局只读三常量)时保留块级绑定承载 + /// 支臂闭包,避免声明点覆写外层绑定。门控只作用于 `if` 支臂下的叶子, + /// 块直接子不受影响。 /// - 支臂为容器(循环/`try`/`with`/标签/显式块)时按各自作用域规则继续下钻, /// 使嵌套 `if` 支臂的函数声明同样到达门控。 /// - 与 lexical 预声明的顺序:本函数在前,`{ let g; function g(){} }` 时 @@ -263,11 +264,13 @@ impl Emitter { } } - /// `if` 支臂裸函数声明的名是否已有外层 var/函数绑定承载:非 eval 脚本顶层的 - /// 非抑制名由实例化阶段建外层 var 绑定,唯一例外是脚本顶层的只读三常量 - /// (全局属性不可配置,声明实例化刻意不建外层绑定),其名须退回块级绑定。 + /// `if` 支臂裸函数声明的名是否已有外层 var/函数绑定承载:非 eval 脚本顶层 + /// 与 sloppy eval 顶层的非抑制名由实例化阶段建外层 var 绑定(eval 面依 + /// Annex B.3.3.3),唯一例外是脚本顶层的只读三常量(全局属性不可配置, + /// 声明实例化刻意不建外层绑定),其名须退回块级绑定。strict eval 不建外层 + /// 承载(其块函数绑定 eval 自身词法环境),与 eval 非顶层同判。 fn if_arm_has_outer_var_carrier(ctx: &CompileCtx, name: &str) -> bool { - !ctx.is_eval_script + (!ctx.is_eval_script || !ctx.is_strict) && !ctx.block_fn_suppressed.contains(name) && !(ctx.is_global_scope && CompileCtx::is_non_writable_global_builtin(name)) } diff --git a/crates/oxide_emit/src/program.rs b/crates/oxide_emit/src/program.rs index 431ec78c..bd5eac3b 100644 --- a/crates/oxide_emit/src/program.rs +++ b/crates/oxide_emit/src/program.rs @@ -314,12 +314,13 @@ impl Emitter { self.predeclare_lexical_declarations(&program.body, &mut ctx, global_lexical)?; // 顶层块级函数名按 sloppy 模式下与浏览器/web 实现惯例兼容的行为处理 - // (块级函数声明建外层 var 绑定并求值写回,非 eval):实例化 var 绑定 + // (块级函数声明建外层 var 绑定并求值写回;sloppy eval 代码同走 + // Annex B.3.3.3,其名并入 eval 的 var 环境):实例化 var 绑定 // (新建 var 槽;顶层只读三常量名不可声明不建)、并入顶层 var 名集 // (裸读/写路由全局对象属性)、GlobalDeclarationInstantiation 序言建属性 - // (define-if-absent,既有属性不改动值)。 + // (define-if-absent,既有属性不改动值)。strict 代码不建。 let mut block_fn_names: Vec = Vec::new(); - if !ctx.is_strict && !ctx.is_eval_script { + if !ctx.is_strict { ctx.block_fn_suppressed = self.collect_block_fn_suppressed_names(&program.body, &ctx.param_names); block_fn_names = self.collect_block_function_names(&program.body); for name in &block_fn_names { diff --git a/crates/oxide_vm/tests/eval_block_fn_tests.rs b/crates/oxide_vm/tests/eval_block_fn_tests.rs new file mode 100644 index 00000000..db3c0fb0 --- /dev/null +++ b/crates/oxide_vm/tests/eval_block_fn_tests.rs @@ -0,0 +1,136 @@ +//! sloppy eval 顶层块级函数声明的 web-compat 外层绑定:Annex B.3.3.3 要求 +//! eval 代码中不属于直接词汇声明的块级函数名并入 eval var 环境,间接/全局 +//! 口径下即全局对象可配置属性。覆盖块形/嵌套块/标签、`if` 支臂、声明前读、 +//! 描述符、delete、同名 var/let 碰撞、strict 守卫与可写内置名。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + vm.run(&Arc::new(module)) +} + +/// 求值并把结果按字符串提取(`JsValue` Display 不携带字符串内容)。 +fn eval_str(source: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module)).expect("run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + +fn assert_bool(result: Result) { + let value = result.unwrap(); + assert!(value.is_bool() && value.as_bool(), "expected true, got {:?}", value); +} + +// ── 主形:eval 顶层块函数泄漏到全局,调用方 typeof 见 function ── +#[test] +fn eval_block_function_leaks_to_global_typeof() { + let s = eval_str("eval('{function g(){}}'); typeof g"); + assert_eq!(s, "function", "eval 顶层块函数应泄漏到全局,实际 {s}"); +} + +// ── 反射面:globalThis.g 可见 ── +#[test] +fn eval_block_function_visible_on_global_object() { + assert_bool(eval("eval('{function g(){}}'); typeof globalThis.g === 'function'")); +} + +// ── 描述符:eval 全局属性可写/可枚举/可配置 ── +#[test] +fn eval_block_function_global_descriptor_all_true() { + let s = eval_str( + "eval('{function g(){}}'); \ + var d = Object.getOwnPropertyDescriptor(globalThis, 'g'); \ + String(d.writable) + ':' + String(d.enumerable) + ':' + String(d.configurable)", + ); + assert_eq!(s, "true:true:true", "eval 块函数全局属性描述符应全 true,实际 {s}"); +} + +// ── 声明前读:var 绑定在求值前实例化为 undefined,属性已存在 ── +#[test] +fn eval_block_function_binding_created_before_declaration() { + let r = eval( + "var t = eval('var t = typeof f; {function f(){}}; t'); \ + t === 'undefined' && ('f' in globalThis)", + ); + assert_bool(r); +} + +// ── 嵌套块:内层块函数的泄漏名仍并入 eval var 环境 ── +#[test] +fn eval_nested_block_function_leaks_to_global() { + let s = eval_str("eval('{{function g(){}}}'); typeof g"); + assert_eq!(s, "function", "eval 嵌套块函数应泄漏到全局,实际 {s}"); +} + +// ── 标签包裹:块内标签直接子函数同走外层绑定 ── +#[test] +fn eval_labeled_block_function_leaks_to_global() { + let s = eval_str("eval('{ l: function g(){} }'); typeof g"); + assert_eq!(s, "function", "eval 块内标签函数应泄漏到全局,实际 {s}"); +} + +// ── 顶层标签:无块包裹的标签函数声明名同样并入 eval var 环境 ── +#[test] +fn eval_top_level_labeled_function_leaks_to_global() { + let s = eval_str("eval('l: function g(){}'); typeof g"); + assert_eq!(s, "function", "eval 顶层标签函数应泄漏到全局,实际 {s}"); +} + +// ── delete:eval 顶层名走可删探测,属性 configurable:true 故可删 ── +#[test] +fn eval_block_function_global_property_deletable() { + assert_bool(eval("eval('{function g(){}}'); delete g")); +} + +// ── if 支臂:支臂函数声明在 eval 顶层有外层 var 承载,不以块 Let 遮蔽 ── +#[test] +fn eval_if_arm_function_leaks_to_global() { + let s = eval_str("eval('{ if(true) function g(){} }'); typeof g"); + assert_eq!(s, "function", "eval 顶层 if 支臂函数应泄漏到全局,实际 {s}"); +} + +// ── if 支臂赋值:支臂后赋值写外层 var(全局属性),非支臂块 Let ── +#[test] +fn eval_if_arm_function_assignment_writes_global() { + let s = eval_str("eval('{ if(true) function g(){}; g = 123; }'); typeof globalThis.g"); + assert_eq!(s, "number", "支臂后赋值应写全局属性,实际 {s}"); +} + +// ── var 碰撞:同名 var 存在时块函数写回该 var,最终值为函数 ── +#[test] +fn eval_block_function_overrides_same_name_var() { + let s = eval_str("eval('var f=1; {function f(){}}'); typeof f"); + assert_eq!(s, "function", "同名 var 应被块函数写回覆盖,实际 {s}"); +} + +// ── let 碰撞守卫:同名词法声明抑制外层绑定,块函数不覆写词法值 ── +#[test] +fn eval_block_function_let_collision_keeps_lexical() { + let s = eval_str("eval('let f=1; {function f(){}} typeof f')"); + assert_eq!(s, "number", "同名 let 不应被块函数覆写,实际 {s}"); +} + +// ── strict 守卫:strict eval 的块函数不泄漏全局 ── +#[test] +fn strict_eval_block_function_does_not_leak() { + let s = eval_str("eval(\"'use strict'; {function g(){}}\"); typeof g"); + assert_eq!(s, "undefined", "strict eval 块函数不应泄漏全局,实际 {s}"); +} + +// ── 可写内置名:块函数覆写 parseInt 全局属性,调用取覆写值 ── +#[test] +fn eval_block_function_overrides_writable_builtin() { + let s = eval_str("eval('{function parseInt(){return \"z\"}}'); parseInt('42')"); + assert_eq!(s, "z", "块函数应覆写可写内置名,实际 {s}"); +}