mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 17:02:03 -06:00
Listing a String object's index names for for-in built one name per UTF-16 unit before any limit check, so a string within the heap limit could make the loop allocate many times that limit. The heap budget for all the names is now checked first, after a collection if needed, and each name takes a unit of fuel. A wrapper made by ToObject stays rooted while the names are listed.
563 lines
27 KiB
Rust
563 lines
27 KiB
Rust
use std::rc::Rc;
|
|
|
|
use proptest::prelude::*;
|
|
|
|
use crate::bytecode::{Code, Handler, Op, Source, verify};
|
|
use crate::{JsValue, LimitKind, Limits, Options, Realm, RunError, RunErrorKind};
|
|
|
|
fn run(src: &str) -> JsValue {
|
|
match Realm::new().run(src) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{src}\n{e:?}"),
|
|
}
|
|
}
|
|
|
|
fn num(src: &str) -> f64 {
|
|
match run(src) {
|
|
JsValue::Number(n) => n,
|
|
other => panic!("{src}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn string(src: &str) -> String {
|
|
match run(src) {
|
|
JsValue::String(s) => s,
|
|
other => panic!("{src}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn truth(src: &str) {
|
|
assert_eq!(run(src), JsValue::Bool(true), "{src}");
|
|
}
|
|
|
|
fn fail(src: &str) -> RunError {
|
|
match Realm::new().run(src) {
|
|
Ok(v) => panic!("{src} completed with {v:?}"),
|
|
Err(e) => e,
|
|
}
|
|
}
|
|
|
|
fn small(limits: Limits) -> Realm {
|
|
Realm::with_options(Options::default(), limits)
|
|
}
|
|
|
|
// ---- language ----
|
|
|
|
#[test]
|
|
fn assignments_resolve_the_variable_before_the_right_hand_side() {
|
|
// Section 11.13.1: the reference is resolved first, so a binding the right-hand side adds
|
|
// does not catch the assignment.
|
|
truth("var x = 0; var scope = {}; with (scope) { x = (scope.x = 2, 1); } scope.x === 2 && x === 1");
|
|
truth("var x = 0; var scope = {}; with (scope) { x += (scope.x = 2, 1); } scope.x === 2 && x === 1");
|
|
truth("var x = 5; function f() { var r = (x = (eval('var x;'), 1)); return [x, r]; } var a = f(); a[0] === undefined && a[1] === 1 && x === 1");
|
|
// Section 11.3.1: the operand's reference is resolved before ToNumber runs.
|
|
truth(
|
|
"var x = 0; var scope = {x: {valueOf: function(){ delete scope.x; return 2; }}}; \
|
|
with (scope) { x++; } scope.x === 3 && x === 0",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn arithmetic_comparison_and_conversions() {
|
|
assert_eq!(num("1 + 2 * 3 - 4 / 2"), 5.0);
|
|
assert_eq!(num("7 % 3 + (-7 % 3)"), 0.0);
|
|
assert_eq!(string("'a' + 1 + 2"), "a12");
|
|
assert_eq!(num("1 + 2 + '3' - 0"), 33.0);
|
|
assert_eq!(num("(1 << 31) >> 31"), -1.0);
|
|
assert_eq!(num("-1 >>> 28"), 15.0);
|
|
assert_eq!(num("~5 ^ 3 | 8 & 12"), -7.0);
|
|
truth("'10' == 10 && null == undefined && !(null == 0) && '' == 0 && '1e1' == 10");
|
|
truth("NaN !== NaN && 0 === -0 && 1/-0 === -Infinity");
|
|
truth("'b' > 'a' && 'B' < 'a' && !(NaN < 1) && !(NaN >= 1) && 2 >= 2 && '10' < '9' && 10 > '9'");
|
|
truth(
|
|
"typeof 1 === 'number' && typeof 'x' === 'string' && typeof null === 'object' && typeof undefined_name === 'undefined' && typeof function(){} === 'function'",
|
|
);
|
|
truth("({valueOf: function(){ return 4; }}) * 2 === 8 && ({toString: function(){ return 'x'; }}) + 'y' === 'xy'");
|
|
truth("+'0x10' === 16 && +' 12 ' === 12 && isNaNish(+'1x'); function isNaNish(n) { return n !== n; }");
|
|
}
|
|
|
|
#[test]
|
|
fn variables_functions_and_closures() {
|
|
assert_eq!(num("var x = 1; function f() { return x + 1; } f()"), 2.0);
|
|
assert_eq!(num("function counter() { var n = 0; return function() { return ++n; }; } var c = counter(); c(); c(); c()"), 3.0);
|
|
assert_eq!(num("f(); function f() { return 5; }"), 5.0);
|
|
assert_eq!(num("var g = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); }; g(10)"), 3_628_800.0);
|
|
assert_eq!(num("function f(a, b) { return arguments.length * 10 + (b === undefined ? 1 : 0); } f(1)"), 11.0);
|
|
assert_eq!(num("function f(a) { arguments[0] = 9; return a; } f(1)"), 9.0);
|
|
assert_eq!(num("function f(a) { 'use strict'; arguments[0] = 9; return a; } f(1)"), 1.0);
|
|
assert_eq!(num("function f(a) { a = 4; return arguments[0]; } f(1)"), 4.0);
|
|
truth("var o = { m: function() { return this; } }; o.m() === o");
|
|
truth("function f() { return this; } f() === this");
|
|
truth("function f() { 'use strict'; return this; } f() === undefined");
|
|
assert_eq!(num("function P(x) { this.x = x; } P.prototype.get = function() { return this.x; }; new P(7).get()"), 7.0);
|
|
truth("function P() { return {a: 1}; } new P().a === 1 && new P() instanceof Object");
|
|
truth("function P() {} var p = new P(); p instanceof P && p.constructor === P && P.prototype.isPrototypeOf(p)");
|
|
assert_eq!(num("(function(a, b, c) {}).length"), 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn call_and_apply_set_this_and_arguments() {
|
|
assert_eq!(num("function f(a, b) { return this.k + a + b; } f.call({k: 1}, 2, 3)"), 6.0);
|
|
assert_eq!(num("function f(a, b) { return this.k + a + b; } f.apply({k: 1}, [2, 3])"), 6.0);
|
|
assert_eq!(num("function f() { return arguments.length; } f.apply(null, undefined)"), 0.0);
|
|
assert_eq!(num("Function.prototype.call.call(function(a) { return a; }, null, 8)"), 8.0);
|
|
let e = fail("Function.prototype.call.call(1)");
|
|
assert_eq!(e.kind, RunErrorKind::Thrown);
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
}
|
|
|
|
#[test]
|
|
fn objects_arrays_and_accessors() {
|
|
assert_eq!(num("var o = {a: 1, 'b': 2, 3: 4}; o.a + o['b'] + o[3]"), 7.0);
|
|
assert_eq!(num("var a = [1, , 3]; a.length"), 3.0);
|
|
assert_eq!(num("var a = []; a[5] = 1; a.length"), 6.0);
|
|
assert_eq!(num("var a = [1, 2, 3, 4]; a.length = 1; a[2] === undefined ? a.length : -1"), 1.0);
|
|
assert_eq!(num("var o = { get x() { return this.y * 2; }, set x(v) { this.y = v; }, y: 1 }; o.x = 5; o.x"), 10.0);
|
|
truth("var o = {a: 1}; delete o.a && !('a' in o) && o.a === undefined");
|
|
truth("'length' in [] && !delete [].length");
|
|
truth("'abc'.length === 3 && 'abc'[1] === 'b' && 'abc'.x === undefined");
|
|
truth("var o = {}; o.hasOwnProperty('x') === false && ({x: 1}).hasOwnProperty('x')");
|
|
truth("Object.prototype.toString.call([]) === '[object Array]' && Object.prototype.toString.call(null) === '[object Null]'");
|
|
let e = fail("var a = []; a.length = -1");
|
|
assert!(e.message.starts_with("RangeError"), "{}", e.message);
|
|
let e = fail("'use strict'; var o = { get x() { return 1; } }; o.x = 2;");
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
let e = fail("null.x");
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
}
|
|
|
|
#[test]
|
|
fn for_in_lists_own_properties_in_insertion_order_then_inherited_ones() {
|
|
assert_eq!(
|
|
string("function P() { this.z = 1; this.a = 2; } P.prototype.m = 3; P.prototype.a = 9; var s = ''; for (var k in new P()) s += k; s"),
|
|
"zam"
|
|
);
|
|
assert_eq!(string("var o = {a: 1, b: 2, c: 3}, s = ''; for (var k in o) { delete o.b; s += k; } s"), "ac");
|
|
assert_eq!(string("var s = ''; for (var k in 'xyz') s += k; s"), "012");
|
|
assert_eq!(num("var n = 0; for (var k in null) n++; for (k in undefined) n++; n"), 0.0);
|
|
assert_eq!(string("var o = {p: 1}, t = {}, s = ''; for (t.k in o) s += t.k; s"), "p");
|
|
assert_eq!(string("var a = ['x', 'y'], s = ''; for (var i in a) s += i + a[i]; s"), "0x1y");
|
|
}
|
|
|
|
#[test]
|
|
fn control_flow_with_labels_switch_and_finally() {
|
|
assert_eq!(
|
|
num(
|
|
"var n = 0; outer: for (var i = 0; i < 5; i++) { for (var j = 0; j < 5; j++) { if (j == 2) continue outer; if (i == 3) break outer; n++; } } n"
|
|
),
|
|
6.0
|
|
);
|
|
assert_eq!(num("var r = 0; switch (3) { case 1: r = 1; case 3: r += 3; case 4: r += 4; break; default: r = 100; } r"), 7.0);
|
|
assert_eq!(num("var r = 0; switch ('z') { case 'a': r = 1; break; default: r = 2; case 'b': r += 10; } r"), 12.0);
|
|
assert_eq!(num("var log = 0; function f() { try { return 1; } finally { log = 5; } } f() + log"), 6.0);
|
|
assert_eq!(num("function f() { try { return 1; } finally { return 2; } } f()"), 2.0);
|
|
assert_eq!(num("var n = 0; for (var i = 0; i < 3; i++) { try { if (i == 1) continue; n += 10; } finally { n += 1; } } n"), 23.0);
|
|
assert_eq!(num("var n = 0; while (true) { try { try { break; } finally { n += 1; } } finally { n += 10; } } n"), 11.0);
|
|
assert_eq!(num("var n = 0; try { throw 3; } catch (e) { n = e; } finally { n *= 2; } n"), 6.0);
|
|
assert_eq!(num("var n = 0; try { try { throw 1; } finally { n = 7; } } catch (e) { n += e; } n"), 8.0);
|
|
assert_eq!(num("function f() { for (var k in {a: 1}) { try { return 4; } finally { } } } f()"), 4.0);
|
|
assert_eq!(num("var x = 0; lbl: { x = 1; break lbl; x = 2; } x"), 1.0);
|
|
assert_eq!(num("var i = 0; do { i++; } while (i < 10); i"), 10.0);
|
|
assert_eq!(num("var e = 1; try { throw 2; } catch (e) { e = 3; } e"), 1.0);
|
|
assert_eq!(num("function f() { var r = 0; with ({a: 1}) { try { return a; } finally { r = 1; } } } f()"), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn with_eval_and_new_function() {
|
|
assert_eq!(num("var o = {a: 2}; with (o) { a = 3; } o.a"), 3.0);
|
|
truth("var o = {f: function() { return this; }}; with (o) { f() === o }");
|
|
assert_eq!(num("var x = 1; function f() { var x = 2; return eval('x'); } f()"), 2.0);
|
|
assert_eq!(num("var x = 1; function f() { var x = 2; return (0, eval)('x'); } f()"), 1.0);
|
|
assert_eq!(num("function f() { eval('var y = 4'); return y; } f()"), 4.0);
|
|
truth("function f() { 'use strict'; eval('var y = 4'); return typeof y; } f() === 'undefined'");
|
|
assert_eq!(num("eval('1; 2; if (false) {}')"), 2.0);
|
|
assert_eq!(num("new Function('a', 'b', 'return a * b;')(6, 7)"), 42.0);
|
|
assert_eq!(num("Function('return 3')()"), 3.0);
|
|
let e = fail("new Function('a){}; (function(', 'return 1')");
|
|
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
|
let e = fail("new Function('}); (function() {')");
|
|
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
|
let e = fail("eval('var = 1')");
|
|
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
|
truth("try { eval('a b') } catch (e) { e instanceof SyntaxError }");
|
|
assert_eq!(string("(function f(a) { return a; }).toString()"), "function f(a) { return a; }");
|
|
}
|
|
|
|
#[test]
|
|
fn strict_mode_assignments_and_errors() {
|
|
let e = fail("'use strict'; undeclared = 1;");
|
|
assert!(e.message.starts_with("ReferenceError"), "{}", e.message);
|
|
assert_eq!(num("undeclared = 1; undeclared"), 1.0);
|
|
let e = fail("'use strict'; (function f() { f = 1; })()");
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
assert_eq!(num("var g = function f() { f = 1; return typeof f === 'function' ? 1 : 0; }; g()"), 1.0);
|
|
let e = fail("'use strict'; NaN = 1;");
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
let e = fail("'use strict'; (function() {}).caller");
|
|
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
|
truth("var o = {}; try { o.x.y; false } catch (e) { e instanceof TypeError && e.constructor === TypeError }");
|
|
truth(
|
|
"try { nope; } catch (e) { e instanceof ReferenceError && e.name === 'ReferenceError' && String_(e) === 'ReferenceError: nope is not defined' } function String_(v) { return '' + v; }",
|
|
);
|
|
truth(
|
|
"new Error('m').message === 'm' && Error('m') instanceof Error && new TypeError().message === '' && new RangeError('r').toString() === 'RangeError: r'",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_carry_line_and_column() {
|
|
let e = fail("var a = 1;\nvar b = 2;\n nope();");
|
|
assert_eq!((e.kind, e.line, e.column), (RunErrorKind::Thrown, 3, 3));
|
|
assert_eq!(e.message, "ReferenceError: nope is not defined");
|
|
let e = fail("function f() {\n throw new TypeError('bad');\n}\nf();");
|
|
assert_eq!((e.line, e.column), (2, 3));
|
|
assert_eq!(e.message, "TypeError: bad");
|
|
let e = fail("var x = ;");
|
|
assert_eq!((e.kind, e.line), (RunErrorKind::Syntax, 1));
|
|
let e = fail("throw 42");
|
|
assert_eq!(e.message, "42");
|
|
}
|
|
|
|
#[test]
|
|
fn host_functions_receive_arguments_and_return_values() {
|
|
let mut realm = Realm::new();
|
|
realm.define_function("add", |args| match args {
|
|
[JsValue::Number(a), JsValue::Number(b)] => Ok(JsValue::Number(a + b)),
|
|
_ => Err("add takes two numbers".into()),
|
|
});
|
|
assert_eq!(realm.run("add(2, 3)"), Ok(JsValue::Number(5.0)));
|
|
let e = realm.run("add('x')").unwrap_err();
|
|
assert_eq!(e.message, "Error: add takes two numbers");
|
|
assert_eq!(realm.run("try { add(); } catch (e) { e.message }"), Ok(JsValue::String("add takes two numbers".into())));
|
|
assert_eq!(realm.run("var kept = 1; 'next'"), Ok(JsValue::String("next".into())));
|
|
assert_eq!(realm.run("kept"), Ok(JsValue::Number(1.0)), "a realm keeps its globals between runs");
|
|
}
|
|
|
|
// ---- limits ----
|
|
|
|
fn limit(realm: &mut Realm, src: &str) -> LimitKind {
|
|
match realm.run(src) {
|
|
Err(RunError { kind: RunErrorKind::Limit(k), .. }) => k,
|
|
other => panic!("{src}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_infinite_loop_ends_with_the_fuel_limit() {
|
|
let mut realm = small(Limits { fuel: 100_000, ..Limits::default() });
|
|
assert_eq!(limit(&mut realm, "for (;;) {}"), LimitKind::Fuel);
|
|
assert_eq!(limit(&mut realm, "while (true) { try { for (;;) {} } catch (e) {} }"), LimitKind::Fuel);
|
|
assert_eq!(limit(&mut realm, "var n = 0; try { for (;;) {} } finally { n = 1; } "), LimitKind::Fuel);
|
|
assert_eq!(
|
|
limit(&mut realm, "function f() { try { for (;;) {} } finally { for (;;) {} } } while (true) { try { f(); } catch (e) {} }"),
|
|
LimitKind::Fuel
|
|
);
|
|
assert_eq!(realm.run("n"), Ok(JsValue::Number(0.0)), "the finally block did not run");
|
|
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)), "each run gets fresh fuel");
|
|
}
|
|
|
|
#[test]
|
|
fn deep_recursion_ends_with_the_frame_limit_on_a_small_stack() {
|
|
let handle = std::thread::Builder::new()
|
|
.stack_size(256 << 10)
|
|
.spawn(|| {
|
|
let mut realm = Realm::new();
|
|
let r = realm.run("function f(n) { return n === 0 ? 0 : 1 + f(n - 1); } f(1000000)");
|
|
let ok = realm.run("function g(n) { return n === 0 ? 0 : 1 + g(n - 1); } g(5000)");
|
|
let caught = realm.run("function h(n) { try { return h(n + 1); } catch (e) { return -1; } } h(0)");
|
|
(r, ok, caught)
|
|
})
|
|
.unwrap();
|
|
let (r, ok, caught) = handle.join().unwrap();
|
|
assert_eq!(r.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Frames)));
|
|
assert_eq!(ok, Ok(JsValue::Number(5000.0)));
|
|
assert_eq!(caught.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Frames)), "catch cannot swallow the frame limit");
|
|
}
|
|
|
|
#[test]
|
|
fn reentry_through_getters_ends_with_the_reentry_limit() {
|
|
let handle = std::thread::Builder::new()
|
|
.stack_size(8 << 20)
|
|
.spawn(|| {
|
|
let mut realm = Realm::new();
|
|
let getter = realm.run("var o = { get x() { return this.x; } }; o.x");
|
|
let conversion = realm.run("var v = { valueOf: function() { return v * 1; } }; v * 1");
|
|
let eval = realm.run("function e() { return (0, eval)('e()'); } e()");
|
|
(getter, conversion, eval)
|
|
})
|
|
.unwrap();
|
|
let (getter, conversion, eval) = handle.join().unwrap();
|
|
for r in [getter, conversion, eval] {
|
|
assert_eq!(r.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Reentry)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn memory_bombs_end_with_the_heap_limit() {
|
|
let limits = Limits { heap_bytes: 4 << 20, ..Limits::default() };
|
|
for src in [
|
|
"var a = null; for (;;) a = {next: a, v: [a, a]};",
|
|
"var s = 'x'; for (;;) s = s + s;",
|
|
"var a = []; for (var i = 0; ; i++) a[i] = 'item' + i;",
|
|
"var s = 'x'; for (;;) { s = s + s; var o = {}; o[s] = 1; }",
|
|
] {
|
|
let mut realm = small(limits.clone());
|
|
assert_eq!(limit(&mut realm, src), LimitKind::Heap, "{src}");
|
|
// What the bomb built is still reachable from the globals; raising the limit lets the
|
|
// realm drop it.
|
|
realm.set_limits(Limits::default());
|
|
assert_eq!(realm.run("a = s = null; 1"), Ok(JsValue::Number(1.0)));
|
|
realm.set_limits(limits.clone());
|
|
assert_eq!(realm.run("2"), Ok(JsValue::Number(2.0)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn garbage_is_collected_cycles_included() {
|
|
let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() });
|
|
let src = "for (var i = 0; i < 200000; i++) { var a = {}, b = {a: a, s: 'x' + i}; a.b = b; } i";
|
|
assert_eq!(realm.run(src), Ok(JsValue::Number(200_000.0)));
|
|
assert!(realm.interp().heap.collections > 0);
|
|
let before = realm.collect_garbage();
|
|
realm.run("var keep = []; for (var j = 0; j < 1000; j++) keep[j] = {j: j};").unwrap();
|
|
let live = realm.collect_garbage();
|
|
realm.run("keep = null;").unwrap();
|
|
let after = realm.collect_garbage();
|
|
assert!(live > before && after < live, "{before} {live} {after}");
|
|
let cells = realm.interp().heap.live_cells();
|
|
assert!(cells < 200, "{cells} cells are still live");
|
|
}
|
|
|
|
#[test]
|
|
fn a_million_appends_flatten_without_recursion() {
|
|
let handle = std::thread::Builder::new()
|
|
.stack_size(1 << 20)
|
|
.spawn(|| {
|
|
let mut realm = Realm::new();
|
|
realm.run("var s = '', t = ''; for (var i = 0; i < 1000000; i++) { s += 'ab'; t = t + 'ab'; } s.length === 2000000 && s === t && s[1999999] === 'b'")
|
|
})
|
|
.unwrap();
|
|
assert_eq!(handle.join().unwrap(), Ok(JsValue::Bool(true)));
|
|
}
|
|
|
|
#[test]
|
|
fn the_interrupt_flag_stops_a_run() {
|
|
let mut realm = Realm::new();
|
|
let flag = realm.interrupt();
|
|
flag.set();
|
|
assert_eq!(limit(&mut realm, "1 + 1"), LimitKind::Interrupted);
|
|
flag.clear();
|
|
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
|
|
let from_host = realm.interrupt();
|
|
realm.define_function("stop", move |_| {
|
|
from_host.set();
|
|
Ok(JsValue::Undefined)
|
|
});
|
|
assert_eq!(limit(&mut realm, "for (var i = 0; ; i++) { if (i === 1000) stop(); }"), LimitKind::Interrupted);
|
|
assert!(flag.is_set());
|
|
assert_eq!(limit(&mut realm, "i"), LimitKind::Interrupted, "the flag stays set");
|
|
flag.clear();
|
|
assert_eq!(realm.run("i"), Ok(JsValue::Number(1000.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn eval_and_function_source_counts_against_the_source_limit() {
|
|
let mut realm = Realm::with_options(Options { max_source_units: 200, ..Options::default() }, Limits::default());
|
|
assert_eq!(realm.run("eval('1 + 1')"), Ok(JsValue::Number(2.0)));
|
|
let e = realm.run("var s = 'x'; for (var i = 0; i < 8; i++) s += s; eval(s)").unwrap_err();
|
|
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
|
|
let e = realm.run("for (var i = 0; i < 20; i++) eval('1 + 1 + 1 + 1 + 1 + 1 + 1 + 1');").unwrap_err();
|
|
assert!(e.message.contains(crate::TOO_LONG), "the sources of one run count together: {e:?}");
|
|
let e = realm.run("Function('return 1;' + ' ')").unwrap_err();
|
|
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_script_longer_than_the_source_limit_is_a_syntax_error() {
|
|
let mut realm = Realm::with_options(Options { max_source_units: 10, ..Options::default() }, Limits::default());
|
|
let e = realm.run("1 + 2 + 3 + 4").unwrap_err();
|
|
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Syntax, true), "{e:?}");
|
|
// Ten UTF-16 code units in 18 bytes of UTF-8 fit.
|
|
assert_eq!(realm.run("'\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}'"), Ok(JsValue::String("\u{e9}".repeat(8))));
|
|
let mut realm = Realm::with_options(Options { max_source_units: 40, ..Options::default() }, Limits::default());
|
|
let e = realm.run("Function('return 1;')").unwrap_err();
|
|
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Thrown, true), "{e:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn bind_last_thrown_binds_undefined_after_a_run_that_did_not_throw() {
|
|
let mut realm = Realm::new();
|
|
assert_eq!(realm.run("throw 'first'").map_err(|e| e.kind), Err(RunErrorKind::Thrown));
|
|
realm.bind_last_thrown("caught");
|
|
assert_eq!(realm.run("caught"), Ok(JsValue::String("first".into())));
|
|
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
|
|
realm.bind_last_thrown("caught");
|
|
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined));
|
|
let _ = realm.run("throw 'second'");
|
|
assert_eq!(realm.run("var").map_err(|e| e.kind), Err(RunErrorKind::Syntax));
|
|
realm.bind_last_thrown("caught");
|
|
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that does not parse throws nothing");
|
|
realm.set_limits(Limits { fuel: 100, ..Limits::default() });
|
|
let _ = realm.run("throw 'third'");
|
|
assert_eq!(limit(&mut realm, "for (;;) {}"), LimitKind::Fuel);
|
|
realm.bind_last_thrown("caught");
|
|
realm.set_limits(Limits::default());
|
|
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that reached a limit threw nothing");
|
|
}
|
|
|
|
#[test]
|
|
fn a_strict_realm_runs_scripts_as_strict_mode_code() {
|
|
let mut realm = Realm::with_options(Options { strict: true, ..Options::default() }, Limits::default());
|
|
assert_eq!(realm.run("with ({}) {}").map_err(|e| e.kind), Err(RunErrorKind::Syntax));
|
|
assert_eq!(realm.run("(function () { return this; })()"), Ok(JsValue::Undefined));
|
|
assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("undefined".into())));
|
|
let mut realm = Realm::new();
|
|
assert_eq!(realm.run("with ({}) {} 1"), Ok(JsValue::Number(1.0)));
|
|
assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("object".into())));
|
|
}
|
|
|
|
#[test]
|
|
fn an_apply_that_throws_while_reading_its_arguments_keeps_nothing_alive() {
|
|
// Each call reads a fresh array at index 0 before index 1 throws; the arrays must not stay
|
|
// reachable after the call ends.
|
|
let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() });
|
|
let src = "function f() {} var caught = 0; \
|
|
var list = {length: 2, get 0() { var a = []; for (var k = 0; k < 100; k++) a[k] = k; return a; }, get 1() { throw 'stop'; }}; \
|
|
for (var i = 0; i < 5000; i++) { try { f.apply(null, list); } catch (e) { caught++; } } caught";
|
|
assert_eq!(realm.run(src), Ok(JsValue::Number(5000.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn for_in_over_a_long_string_checks_the_heap_before_listing_its_indices() {
|
|
// The string fits in the heap and its 131,072 index names do not. The fuel runs out partway
|
|
// through listing them, so only the check before the listing ends the run with the heap limit.
|
|
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 50_000, ..Limits::default() });
|
|
assert_eq!(limit(&mut realm, "var s = 'x'; while (s.length < 100000) s += s; for (var k in s) {}"), LimitKind::Heap);
|
|
assert_eq!(realm.run("var n = ''; for (var k in 'abc') n += k; n"), Ok(JsValue::String("012".into())));
|
|
}
|
|
|
|
#[test]
|
|
fn for_in_over_a_long_string_takes_fuel_for_each_index() {
|
|
let mut realm = small(Limits { fuel: 10_000, ..Limits::default() });
|
|
assert_eq!(limit(&mut realm, "var s = 'x'; while (s.length < 100000) s += s; for (var k in s) break;"), LimitKind::Fuel);
|
|
}
|
|
|
|
#[test]
|
|
fn wasm_builds_have_smaller_default_limits() {
|
|
let limits = Limits::default();
|
|
#[cfg(target_arch = "wasm32")]
|
|
assert_eq!(limits, Limits { fuel: 250_000_000, heap_bytes: 128 << 20, frames: 2_000, reentry: 16 });
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
assert_eq!(limits, Limits { fuel: 1_000_000_000, heap_bytes: 512 << 20, frames: 10_000, reentry: 32 });
|
|
}
|
|
|
|
#[test]
|
|
fn deep_nesting_compiles_on_a_small_stack() {
|
|
let n = crate::DEFAULT_MAX_DEPTH - 5;
|
|
let cases = [
|
|
format!("x = {}1{};", "[".repeat(n), "]".repeat(n)),
|
|
format!("x = {}1{};", "{a:".repeat(n), "}".repeat(n)),
|
|
format!("{}{}", "function f(){".repeat(n), "}".repeat(n)),
|
|
format!("{}x = 1;", "if (1) ".repeat(n)),
|
|
format!("x = {}1;", "1 ? 1 : ".repeat(n)),
|
|
format!("x = 0; {}x++;{}", "while (x < 1) { try { ".repeat(n / 2), " } finally { } }".repeat(n / 2)),
|
|
];
|
|
let handle = std::thread::Builder::new()
|
|
.stack_size(1 << 20)
|
|
.spawn(move || cases.iter().map(|src| Realm::new().run(src).map(|_| ()).map_err(|e| e.kind)).collect::<Vec<_>>())
|
|
.unwrap();
|
|
for r in handle.join().unwrap() {
|
|
assert!(r.is_ok() || r == Err(RunErrorKind::Syntax), "{r:?}");
|
|
}
|
|
let long = [
|
|
format!("x = 1{};", " + 1".repeat(100_000)),
|
|
format!("{}x = 1;", "a = ".repeat(100_000)),
|
|
format!("x = {{}}; x.y = x; x{};", ".y".repeat(100_000)),
|
|
];
|
|
for src in &long {
|
|
Realm::new().run(src).unwrap();
|
|
}
|
|
assert_eq!(num(&format!("var a; {}1; a", "a = ".repeat(100_000))), 1.0);
|
|
assert_eq!(num(&format!("x = 1{}", " + 1".repeat(100_000))), 100_001.0);
|
|
}
|
|
|
|
// ---- the verifier ----
|
|
|
|
fn code(ops: Vec<Op>) -> Code {
|
|
let source = Rc::new(Source { lines: crate::LineTable::new(&[]), text: Rc::from(Vec::<u16>::new()) });
|
|
Code {
|
|
positions: vec![0; ops.len()],
|
|
ops,
|
|
numbers: vec![1.0],
|
|
strings: Vec::new(),
|
|
names: Vec::new(),
|
|
functions: Vec::new(),
|
|
regexps: Vec::new(),
|
|
handlers: Vec::new(),
|
|
finally_tables: Vec::new(),
|
|
max_stack: 0,
|
|
strict: false,
|
|
self_name: None,
|
|
params: Vec::new(),
|
|
vars: Vec::new(),
|
|
fun_decls: Vec::new(),
|
|
uses_arguments: false,
|
|
source,
|
|
span: (0, 0),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_verifier_rejects_bad_bytecode() {
|
|
assert_eq!(verify(&code(vec![Op::Number(0), Op::Return])), Ok(1));
|
|
let bad: Vec<(Vec<Op>, &str)> = vec![
|
|
(vec![Op::Jump(9)], "past the end"),
|
|
(vec![Op::Pop, Op::Undefined, Op::Return], "pops below"),
|
|
(vec![Op::Number(5), Op::Return], "number 5 out of range"),
|
|
(vec![Op::GetVar(0), Op::Return], "name 0 out of range"),
|
|
(vec![Op::Closure(0), Op::Return], "function 0 out of range"),
|
|
(vec![Op::Undefined], "past its last instruction"),
|
|
(vec![Op::True, Op::JumpIfFalse(3), Op::Undefined, Op::Undefined, Op::Return], "depths"),
|
|
(vec![Op::Undefined, Op::Undefined, Op::EndFinally(0)], "finally table 0"),
|
|
(vec![], "no instructions"),
|
|
];
|
|
for (ops, want) in bad {
|
|
let e = verify(&code(ops.clone())).unwrap_err();
|
|
assert!(e.contains(want), "{ops:?}: {e}");
|
|
}
|
|
let mut c = code(vec![Op::Undefined, Op::Return]);
|
|
c.handlers.push(Handler { start: 0, end: 5, target: 0, depth: 0, env_depth: 0 });
|
|
assert!(verify(&c).unwrap_err().contains("handler range"));
|
|
}
|
|
|
|
#[test]
|
|
fn every_compiled_function_passes_the_verifier() {
|
|
// Each code is verified as it is compiled; a failure would surface as an internal error.
|
|
let src = "function f(a) { try { for (var k in a) { switch (k) { case 'x': continue; default: break; } } } catch (e) { return e; } finally { a = 0; } \
|
|
with (a) { lbl: do { if (b) break lbl; } while (0); } return function() { return arguments; }; } 1";
|
|
assert_eq!(run(src), JsValue::Number(1.0));
|
|
}
|
|
|
|
// ---- robustness ----
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(256))]
|
|
|
|
#[test]
|
|
fn arbitrary_fragment_programs_never_panic(parts in proptest::collection::vec(0..crate::tests::FRAGMENTS.len(), 0..60)) {
|
|
let src: String = parts.iter().map(|&i| crate::tests::FRAGMENTS[i]).collect();
|
|
let mut realm = small(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 });
|
|
if let Err(e) = realm.run(&src) {
|
|
prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arbitrary_text_never_panics(src in ".{0,120}") {
|
|
let mut realm = small(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 });
|
|
if let Err(e) = realm.run(&src) {
|
|
prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}");
|
|
}
|
|
}
|
|
}
|