//! Deep nesting the expression text check can't see (#2124, #2125): source handed to `eval` / //! `Function` at run time, and deeply nested values that boa's natives (`join`, `JSON.stringify`, //! proxies…) walk recursively. Each used to overflow the stack, which aborts the whole app //! (`catch_unwind` can't catch it); now each is an expression error. use super::*; /// The render threads' stack size (rayon's default): what the app gets. const RENDER_STACK: usize = 2 << 20; /// Evaluate `text` on the title's source text (any result converts) on a render-sized stack. fn eval_small_stack(text: &str) -> Result { let text = text.to_string(); std::thread::Builder::new() .stack_size(RENDER_STACK) .spawn(move || { let f = fx(); f.ev(f.title, "text/sourceText", &text, 0.0) }) .unwrap() .join() .unwrap() } fn text_of(r: Result) -> String { match r { Ok(Value::Text(d)) => d.text, other => panic!("not text: {other:?}"), } } /// A 100 000-deep array: `var a = []; … a = [a]` in a loop (under the loop limit). const DEEP_ARRAY: &str = "var a = []; for (var i = 0; i < 100000; i++) a = [a];"; #[test] fn eval_and_function_constructors_check_nesting() { let deep = r#""(".repeat(1000) + "1" + ")".repeat(1000)"#; for call in [ format!("eval({deep})"), format!("Function({deep})()"), format!("new Function('a', {deep})(1)"), format!("Function('a = ' + {deep}, 'return a')()"), "Function.apply(null, '('.repeat(1000).split('').concat(['1']))".to_string(), format!("(function () {{}}).constructor({deep})()"), format!("Object.getPrototypeOf(function* () {{}}).constructor({deep})"), format!("Object.getPrototypeOf(async function () {{}}).constructor({deep})"), format!("Object.getPrototypeOf(async function* () {{}}).constructor({deep})"), format!("Reflect.construct(Function, [{deep}])"), ] { let e = eval_small_stack(&call).expect_err(&call); assert!(!e.is_empty(), "{call}"); } } #[test] fn eval_and_function_still_work() { let f = fx(); assert_eq!(f.num("eval('1 + 2')", 0.0), 3.0); assert_eq!(f.num("eval('[1, [2, (3)]]')[1][1]", 0.0), 3.0); assert_eq!(f.num("new Function('a', 'b', 'return a * b')(3, 4)", 0.0), 12.0); assert_eq!(f.num("Function('return 5')()", 0.0), 5.0); assert_eq!(f.num("(function () {}).constructor('return 6')()", 0.0), 6.0); assert_eq!(f.num("(function () {}) instanceof Function ? 1 : 0", 0.0), 1.0); assert_eq!(f.num("(function () {}).constructor === Function ? 1 : 0", 0.0), 1.0); assert_eq!(f.num("Object.getPrototypeOf(function* () { yield 7; }).constructor('yield 7')().next().value", 0.0), 7.0); } #[test] fn deep_array_result_is_an_error_or_text_not_a_crash() { // The result itself: detaching it used to `toString` it past depth 4. let _ = eval_small_stack(&format!("{DEEP_ARRAY} a")); // Explicit conversions inside the script. for tail in ["a.join()", "a.toString()", "String(a)", "a + ''", "`${a}`", "a.toLocaleString()", "JSON.stringify(a)", "JSON.stringify({ a: a })"] { let text = format!("{DEEP_ARRAY} {tail}"); assert!(eval_small_stack(&text).is_err(), "{text}"); } // A deep array as a text style value goes through JSON.stringify in the prelude. let _ = eval_small_stack(&format!("{DEEP_ARRAY} text.sourceText.style.setFontSize(a)")); } #[test] fn deep_proxy_chain_is_an_error_not_a_crash() { let text = "var p = {}; for (var i = 0; i < 100000; i++) p = new Proxy(p, {}); p.x"; assert!(eval_small_stack(text).is_err(), "{text}"); // Neither is available to expressions; the object model's own proxies still work. let f = fx(); assert_eq!(f.text("typeof Proxy + ' ' + typeof Reflect", 0.0), "undefined undefined"); assert_eq!(f.num("thisComp.layer('A').effect('Slider Control')('Slider')", 0.0), 42.0); } #[test] fn nested_values_still_convert() { let f = fx(); assert_eq!(f.text("[[1, 2], [3, [4, 5]]].join(';')", 0.0), "1,2;3,4,5"); assert_eq!(f.text("String([[1, 2], 3])", 0.0), "1,2,3"); assert_eq!(f.text("JSON.stringify({ a: [1, { b: [2] }] })", 0.0), r#"{"a":[1,{"b":[2]}]}"#); assert_eq!(f.text("JSON.stringify({ a: 1, b: 2 }, ['b'])", 0.0), r#"{"b":2}"#); assert_eq!(f.text("JSON.stringify([1, [2]], null, 1)", 0.0), "[\n 1,\n [\n 2\n ]\n]"); assert_eq!(f.text("JSON.stringify({ a: 1 }, function (k, v) { return k === 'a' ? v + 1 : v; })", 0.0), r#"{"a":2}"#); // Nested deeper than the detached levels: still converts to its text. assert_eq!(text_of(eval_small_stack("[[[[[[1, 2]]]]]]")), ""); assert_eq!(f.text("String([[[[[[[[1]]]]]]]])", 0.0), "1"); assert_eq!(f.num("[1, [2, [3, [4]]]].flat(Infinity).length", 0.0), 4.0); }