mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
eval, Function and the generator/async function constructors now check the nesting of their source like the expression text. join, toLocaleString, flat and JSON.stringify check the run's stack use per level, results nested past the detached levels become [object], and Proxy/Reflect are no longer available to expressions. Each of these overflowed the stack and aborted the app.
105 lines
4.8 KiB
Rust
105 lines
4.8 KiB
Rust
//! 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<Value, String> {
|
|
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<Value, String>) -> 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);
|
|
}
|