From 802d796d5118ac4fd6d0571e4c4126fc7eb8b695 Mon Sep 17 00:00:00 2001 From: Haled Odat <8566042+HalidOdat@users.noreply.github.com> Date: Fri, 5 May 2023 18:20:54 +0200 Subject: [PATCH] Implement runtime limits for loops (#2857) --- boa_cli/src/debug/limits.rs | 35 ++++++ boa_cli/src/debug/mod.rs | 7 ++ boa_engine/src/builtins/array/tests.rs | 21 ++-- boa_engine/src/builtins/bigint/tests.rs | 48 +++++---- boa_engine/src/builtins/date/tests.rs | 4 +- boa_engine/src/builtins/function/tests.rs | 11 +- boa_engine/src/builtins/json/tests.rs | 4 +- boa_engine/src/builtins/map/tests.rs | 4 +- boa_engine/src/builtins/number/tests.rs | 13 +-- boa_engine/src/builtins/object/tests.rs | 40 ++++--- boa_engine/src/builtins/regexp/tests.rs | 32 ++++-- boa_engine/src/builtins/set/tests.rs | 4 +- boa_engine/src/builtins/string/tests.rs | 28 ++--- boa_engine/src/bytecompiler/statement/loop.rs | 2 + boa_engine/src/context/mod.rs | 31 ++++++ boa_engine/src/environments/tests.rs | 8 +- boa_engine/src/error.rs | 101 +++++++++++++++++- boa_engine/src/lib.rs | 4 +- boa_engine/src/object/tests.rs | 4 +- boa_engine/src/realm.rs | 14 +-- boa_engine/src/tests/control_flow/loops.rs | 10 +- boa_engine/src/tests/control_flow/mod.rs | 8 +- boa_engine/src/tests/function.rs | 16 +-- boa_engine/src/tests/mod.rs | 16 +-- boa_engine/src/tests/operators.rs | 60 +++++------ boa_engine/src/tests/spread.rs | 4 +- boa_engine/src/value/tests.rs | 4 +- boa_engine/src/vm/call_frame/env_stack.rs | 21 +++- boa_engine/src/vm/flowgraph/mod.rs | 1 - boa_engine/src/vm/mod.rs | 24 +++-- boa_engine/src/vm/opcode/call/mod.rs | 16 +-- .../src/vm/opcode/iteration/loop_ops.rs | 65 +++++++---- boa_engine/src/vm/opcode/new/mod.rs | 8 +- boa_engine/src/vm/runtime_limits.rs | 61 +++++++++++ boa_engine/src/vm/tests.rs | 39 ++++++- boa_examples/src/bin/runtime_limits.rs | 47 ++++++++ docs/boa_object.md | 19 +++- 37 files changed, 620 insertions(+), 214 deletions(-) create mode 100644 boa_cli/src/debug/limits.rs create mode 100644 boa_engine/src/vm/runtime_limits.rs create mode 100644 boa_examples/src/bin/runtime_limits.rs diff --git a/boa_cli/src/debug/limits.rs b/boa_cli/src/debug/limits.rs new file mode 100644 index 0000000000..49fff9970d --- /dev/null +++ b/boa_cli/src/debug/limits.rs @@ -0,0 +1,35 @@ +use boa_engine::{ + object::{FunctionObjectBuilder, ObjectInitializer}, + property::Attribute, + Context, JsArgs, JsObject, JsResult, JsValue, NativeFunction, +}; + +fn get_loop(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult { + let max = context.runtime_limits().loop_iteration_limit(); + Ok(JsValue::from(max)) +} + +fn set_loop(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let value = args.get_or_undefined(0).to_length(context)?; + context.runtime_limits_mut().set_loop_iteration_limit(value); + Ok(JsValue::undefined()) +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + let get_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_loop)) + .name("get loop") + .length(0) + .build(); + let set_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_loop)) + .name("set loop") + .length(1) + .build(); + ObjectInitializer::new(context) + .accessor( + "loop", + Some(get_loop), + Some(set_loop), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .build() +} diff --git a/boa_cli/src/debug/mod.rs b/boa_cli/src/debug/mod.rs index 4e2f9409ea..ca1c22d077 100644 --- a/boa_cli/src/debug/mod.rs +++ b/boa_cli/src/debug/mod.rs @@ -5,6 +5,7 @@ use boa_engine::{object::ObjectInitializer, property::Attribute, Context, JsObje mod function; mod gc; +mod limits; mod object; mod optimizer; mod realm; @@ -17,6 +18,7 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject { let optimizer_module = optimizer::create_object(context); let gc_module = gc::create_object(context); let realm_module = realm::create_object(context); + let limits_module = limits::create_object(context); ObjectInitializer::new(context) .property( @@ -49,6 +51,11 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject { realm_module, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) + .property( + "limits", + limits_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) .build() } diff --git a/boa_engine/src/builtins/array/tests.rs b/boa_engine/src/builtins/array/tests.rs index 42d0e49218..71b7b39f3e 100644 --- a/boa_engine/src/builtins/array/tests.rs +++ b/boa_engine/src/builtins/array/tests.rs @@ -1,8 +1,5 @@ use super::Array; -use crate::{ - builtins::{error::ErrorKind, Number}, - run_test_actions, Context, JsValue, TestAction, -}; +use crate::{builtins::Number, run_test_actions, Context, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -191,7 +188,7 @@ fn flat_map_not_callable() { var array = [1,2,3]; array.flatMap("not a function"); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "flatMap mapper function is not callable", )]); } @@ -639,7 +636,7 @@ fn reduce() { // Empty array TestAction::assert_native_error( "[].reduce((acc, x) => acc + x);", - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduce: called on an empty array and with no initial value", ), // Array with no defined elements @@ -650,7 +647,7 @@ fn reduce() { delete deleteArr[1]; deleteArr.reduce((acc, x) => acc + x); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduce: called on an empty array and with no initial value", ), // No callback @@ -659,7 +656,7 @@ fn reduce() { var someArr = [0, 1]; someArr.reduce(''); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduce: callback function is not callable", ), ]); @@ -720,7 +717,7 @@ fn reduce_right() { // Empty array TestAction::assert_native_error( "[].reduceRight((acc, x) => acc + x);", - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduceRight: called on an empty array and with no initial value", ), // Array with no defined elements @@ -731,7 +728,7 @@ fn reduce_right() { delete deleteArr[1]; deleteArr.reduceRight((acc, x) => acc + x); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduceRight: called on an empty array and with no initial value", ), // No callback @@ -740,7 +737,7 @@ fn reduce_right() { var otherArr = [0, 1]; otherArr.reduceRight(""); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Array.prototype.reduceRight: callback function is not callable", ), ]); @@ -865,7 +862,7 @@ fn array_spread_arrays() { fn array_spread_non_iterable() { run_test_actions([TestAction::assert_native_error( "const array2 = [...5];", - ErrorKind::Type, + JsNativeErrorKind::Type, "value with type `number` is not iterable", )]); } diff --git a/boa_engine/src/builtins/bigint/tests.rs b/boa_engine/src/builtins/bigint/tests.rs index f62b95625d..5c0f1894fe 100644 --- a/boa_engine/src/builtins/bigint/tests.rs +++ b/boa_engine/src/builtins/bigint/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsBigInt, TestAction}; +use crate::{run_test_actions, JsBigInt, JsNativeErrorKind, TestAction}; #[test] fn equality() { @@ -62,17 +62,17 @@ fn bigint_function_throws() { run_test_actions([ TestAction::assert_native_error( "BigInt(0.1)", - ErrorKind::Range, + JsNativeErrorKind::Range, "cannot convert 0.1 to a BigInt", ), TestAction::assert_native_error( "BigInt(null)", - ErrorKind::Type, + JsNativeErrorKind::Type, "cannot convert null to a BigInt", ), TestAction::assert_native_error( "BigInt(undefined)", - ErrorKind::Type, + JsNativeErrorKind::Type, "cannot convert undefined to a BigInt", ), ]); @@ -108,29 +108,37 @@ fn operations() { ), TestAction::assert_eq("15000n / 50n", JsBigInt::from(300)), TestAction::assert_eq("15001n / 50n", JsBigInt::from(300)), - TestAction::assert_native_error("1n/0n", ErrorKind::Range, "BigInt division by zero"), + TestAction::assert_native_error( + "1n/0n", + JsNativeErrorKind::Range, + "BigInt division by zero", + ), TestAction::assert_eq("15007n % 10n", JsBigInt::from(7)), - TestAction::assert_native_error("1n % 0n", ErrorKind::Range, "BigInt division by zero"), + TestAction::assert_native_error( + "1n % 0n", + JsNativeErrorKind::Range, + "BigInt division by zero", + ), TestAction::assert_eq( "100n ** 10n", JsBigInt::from_string("100000000000000000000").unwrap(), ), TestAction::assert_native_error( "10n ** (-10n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "BigInt negative exponent", ), TestAction::assert_eq("8n << 2n", JsBigInt::from(32)), TestAction::assert_native_error( "1000n << 1000000000000000n", - ErrorKind::Range, + JsNativeErrorKind::Range, "Maximum BigInt size exceeded", ), TestAction::assert_eq("8n >> 2n", JsBigInt::from(2)), // TODO: this should return 0n instead of throwing TestAction::assert_native_error( "1000n >> 1000000000000000n", - ErrorKind::Range, + JsNativeErrorKind::Range, "Maximum BigInt size exceeded", ), ]); @@ -151,17 +159,17 @@ fn to_string_invalid_radix() { run_test_actions([ TestAction::assert_native_error( "10n.toString(null)", - ErrorKind::Range, + JsNativeErrorKind::Range, "radix must be an integer at least 2 and no greater than 36", ), TestAction::assert_native_error( "10n.toString(-1)", - ErrorKind::Range, + JsNativeErrorKind::Range, "radix must be an integer at least 2 and no greater than 36", ), TestAction::assert_native_error( "10n.toString(37)", - ErrorKind::Range, + JsNativeErrorKind::Range, "radix must be an integer at least 2 and no greater than 36", ), ]); @@ -219,22 +227,22 @@ fn as_int_n_errors() { run_test_actions([ TestAction::assert_native_error( "BigInt.asIntN(-1, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asIntN(-2.5, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asIntN(9007199254740992, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asIntN(0n, 0n)", - ErrorKind::Type, + JsNativeErrorKind::Type, "argument must not be a bigint", ), ]); @@ -292,22 +300,22 @@ fn as_uint_n_errors() { run_test_actions([ TestAction::assert_native_error( "BigInt.asUintN(-1, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asUintN(-2.5, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asUintN(9007199254740992, 0n)", - ErrorKind::Range, + JsNativeErrorKind::Range, "Index must be between 0 and 2^53 - 1", ), TestAction::assert_native_error( "BigInt.asUintN(0n, 0n)", - ErrorKind::Type, + JsNativeErrorKind::Type, "argument must not be a bigint", ), ]); diff --git a/boa_engine/src/builtins/date/tests.rs b/boa_engine/src/builtins/date/tests.rs index 6d337be732..0743c51dbc 100644 --- a/boa_engine/src/builtins/date/tests.rs +++ b/boa_engine/src/builtins/date/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; use indoc::indoc; @@ -47,7 +47,7 @@ fn timestamp_from_utc( fn date_this_time_value() { run_test_actions([TestAction::assert_native_error( "({toString: Date.prototype.toString}).toString()", - ErrorKind::Type, + JsNativeErrorKind::Type, "'this' is not a Date", )]); } diff --git a/boa_engine/src/builtins/function/tests.rs b/boa_engine/src/builtins/function/tests.rs index ef77c4bb91..d5d87131fb 100644 --- a/boa_engine/src/builtins/function/tests.rs +++ b/boa_engine/src/builtins/function/tests.rs @@ -1,11 +1,10 @@ use crate::{ - builtins::error::ErrorKind, error::JsNativeError, js_string, native_function::NativeFunction, object::{FunctionObjectBuilder, JsObject}, property::{Attribute, PropertyDescriptor}, - run_test_actions, JsValue, TestAction, + run_test_actions, JsNativeErrorKind, JsValue, TestAction, }; use indoc::indoc; @@ -60,7 +59,7 @@ fn function_prototype() { ), TestAction::assert_native_error( "new Function.prototype()", - ErrorKind::Type, + JsNativeErrorKind::Type, "not a constructor", ), ]); @@ -81,7 +80,7 @@ fn function_prototype_call_throw() { let call = Function.prototype.call; call(call) "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "undefined is not a function", )]); } @@ -181,12 +180,12 @@ fn function_constructor_early_errors_super() { run_test_actions([ TestAction::assert_native_error( "Function('super()')()", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "invalid `super` call", ), TestAction::assert_native_error( "Function('super.a')()", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "invalid `super` reference", ), ]); diff --git a/boa_engine/src/builtins/json/tests.rs b/boa_engine/src/builtins/json/tests.rs index b65b04253a..72151acf97 100644 --- a/boa_engine/src/builtins/json/tests.rs +++ b/boa_engine/src/builtins/json/tests.rs @@ -1,6 +1,6 @@ use indoc::indoc; -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; #[test] fn json_sanity() { @@ -307,7 +307,7 @@ fn json_fields_should_be_enumerable() { fn json_parse_with_no_args_throws_syntax_error() { run_test_actions([TestAction::assert_native_error( "JSON.parse();", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "expected value at line 1 column 1", )]); } diff --git a/boa_engine/src/builtins/map/tests.rs b/boa_engine/src/builtins/map/tests.rs index ca4f5d0904..75aa377b51 100644 --- a/boa_engine/src/builtins/map/tests.rs +++ b/boa_engine/src/builtins/map/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -242,7 +242,7 @@ fn recursive_display() { fn not_a_function() { run_test_actions([TestAction::assert_native_error( "let map = Map()", - ErrorKind::Type, + JsNativeErrorKind::Type, "calling a builtin Map constructor without new is forbidden", )]); } diff --git a/boa_engine/src/builtins/number/tests.rs b/boa_engine/src/builtins/number/tests.rs index d56db30e98..8e38fe3e78 100644 --- a/boa_engine/src/builtins/number/tests.rs +++ b/boa_engine/src/builtins/number/tests.rs @@ -1,10 +1,7 @@ #![allow(clippy::float_cmp)] use crate::{ - builtins::{error::ErrorKind, Number}, - run_test_actions, - value::AbstractRelation, - TestAction, + builtins::Number, run_test_actions, value::AbstractRelation, JsNativeErrorKind, TestAction, }; #[test] @@ -81,10 +78,10 @@ fn to_precision() { "(1/3).toPrecision(60)", "0.333333333333333314829616256247390992939472198486328125000000", ), - TestAction::assert_native_error("(1).toPrecision(101)", ErrorKind::Range, ERROR), - TestAction::assert_native_error("(1).toPrecision(0)", ErrorKind::Range, ERROR), - TestAction::assert_native_error("(1).toPrecision(-2000)", ErrorKind::Range, ERROR), - TestAction::assert_native_error("(1).toPrecision('%')", ErrorKind::Range, ERROR), + TestAction::assert_native_error("(1).toPrecision(101)", JsNativeErrorKind::Range, ERROR), + TestAction::assert_native_error("(1).toPrecision(0)", JsNativeErrorKind::Range, ERROR), + TestAction::assert_native_error("(1).toPrecision(-2000)", JsNativeErrorKind::Range, ERROR), + TestAction::assert_native_error("(1).toPrecision('%')", JsNativeErrorKind::Range, ERROR), ]); } diff --git a/boa_engine/src/builtins/object/tests.rs b/boa_engine/src/builtins/object/tests.rs index d9901dbc37..b02d9d97bb 100644 --- a/boa_engine/src/builtins/object/tests.rs +++ b/boa_engine/src/builtins/object/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -15,7 +15,7 @@ fn object_create_with_regular_object() { fn object_create_with_undefined() { run_test_actions([TestAction::assert_native_error( "Object.create()", - ErrorKind::Type, + JsNativeErrorKind::Type, "Object prototype may only be an Object or null: undefined", )]); } @@ -24,7 +24,7 @@ fn object_create_with_undefined() { fn object_create_with_number() { run_test_actions([TestAction::assert_native_error( "Object.create(5)", - ErrorKind::Type, + JsNativeErrorKind::Type, "Object prototype may only be an Object or null: 5", )]); } @@ -238,11 +238,19 @@ fn object_get_own_property_names_invalid_args() { const ERROR: &str = "cannot convert 'null' or 'undefined' to object"; run_test_actions([ - TestAction::assert_native_error("Object.getOwnPropertyNames()", ErrorKind::Type, ERROR), - TestAction::assert_native_error("Object.getOwnPropertyNames(null)", ErrorKind::Type, ERROR), + TestAction::assert_native_error( + "Object.getOwnPropertyNames()", + JsNativeErrorKind::Type, + ERROR, + ), + TestAction::assert_native_error( + "Object.getOwnPropertyNames(null)", + JsNativeErrorKind::Type, + ERROR, + ), TestAction::assert_native_error( "Object.getOwnPropertyNames(undefined)", - ErrorKind::Type, + JsNativeErrorKind::Type, ERROR, ), ]); @@ -307,15 +315,19 @@ fn object_get_own_property_symbols_invalid_args() { const ERROR: &str = "cannot convert 'null' or 'undefined' to object"; run_test_actions([ - TestAction::assert_native_error("Object.getOwnPropertySymbols()", ErrorKind::Type, ERROR), + TestAction::assert_native_error( + "Object.getOwnPropertySymbols()", + JsNativeErrorKind::Type, + ERROR, + ), TestAction::assert_native_error( "Object.getOwnPropertySymbols(null)", - ErrorKind::Type, + JsNativeErrorKind::Type, ERROR, ), TestAction::assert_native_error( "Object.getOwnPropertySymbols(undefined)", - ErrorKind::Type, + JsNativeErrorKind::Type, ERROR, ), ]); @@ -382,9 +394,13 @@ fn object_from_entries_invalid_args() { const ERROR: &str = "cannot convert null or undefined to Object"; run_test_actions([ - TestAction::assert_native_error("Object.fromEntries()", ErrorKind::Type, ERROR), - TestAction::assert_native_error("Object.fromEntries(null)", ErrorKind::Type, ERROR), - TestAction::assert_native_error("Object.fromEntries(undefined)", ErrorKind::Type, ERROR), + TestAction::assert_native_error("Object.fromEntries()", JsNativeErrorKind::Type, ERROR), + TestAction::assert_native_error("Object.fromEntries(null)", JsNativeErrorKind::Type, ERROR), + TestAction::assert_native_error( + "Object.fromEntries(undefined)", + JsNativeErrorKind::Type, + ERROR, + ), ]); } diff --git a/boa_engine/src/builtins/regexp/tests.rs b/boa_engine/src/builtins/regexp/tests.rs index b5c59c0026..3ce05fc6e9 100644 --- a/boa_engine/src/builtins/regexp/tests.rs +++ b/boa_engine/src/builtins/regexp/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, object::JsObject, run_test_actions, JsValue, TestAction}; +use crate::{object::JsObject, run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -120,12 +120,12 @@ fn no_panic_on_parse_fail() { run_test_actions([ TestAction::assert_native_error( r"var re = /]/u;", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid regular expression literal: Unbalanced bracket at line 1, col 10", ), TestAction::assert_native_error( r"var re = /a{/u;", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid regular expression literal: Invalid quantifier at line 1, col 10", ), ]); @@ -182,13 +182,25 @@ fn search() { 4, ), // this-val-non-obj - TestAction::assert_native_error("search.value.call()", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call(undefined)", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call(null)", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call(true)", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call('string')", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call(Symbol.search)", ErrorKind::Type, ERROR), - TestAction::assert_native_error("search.value.call(86)", ErrorKind::Type, ERROR), + TestAction::assert_native_error("search.value.call()", JsNativeErrorKind::Type, ERROR), + TestAction::assert_native_error( + "search.value.call(undefined)", + JsNativeErrorKind::Type, + ERROR, + ), + TestAction::assert_native_error("search.value.call(null)", JsNativeErrorKind::Type, ERROR), + TestAction::assert_native_error("search.value.call(true)", JsNativeErrorKind::Type, ERROR), + TestAction::assert_native_error( + "search.value.call('string')", + JsNativeErrorKind::Type, + ERROR, + ), + TestAction::assert_native_error( + "search.value.call(Symbol.search)", + JsNativeErrorKind::Type, + ERROR, + ), + TestAction::assert_native_error("search.value.call(86)", JsNativeErrorKind::Type, ERROR), // u-lastindex-advance TestAction::assert_eq(r"/\udf06/u[Symbol.search]('\ud834\udf06')", -1), TestAction::assert_eq("/a/[Symbol.search](\"a\")", 0), diff --git a/boa_engine/src/builtins/set/tests.rs b/boa_engine/src/builtins/set/tests.rs index 6616a45b05..50f5289ab2 100644 --- a/boa_engine/src/builtins/set/tests.rs +++ b/boa_engine/src/builtins/set/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; use indoc::indoc; #[test] @@ -170,7 +170,7 @@ fn recursive_display() { fn not_a_function() { run_test_actions([TestAction::assert_native_error( "Set()", - ErrorKind::Type, + JsNativeErrorKind::Type, "calling a builtin Set constructor without new is forbidden", )]); } diff --git a/boa_engine/src/builtins/string/tests.rs b/boa_engine/src/builtins/string/tests.rs index 45bca332c4..b5418286a1 100644 --- a/boa_engine/src/builtins/string/tests.rs +++ b/boa_engine/src/builtins/string/tests.rs @@ -1,6 +1,6 @@ use indoc::indoc; -use crate::{builtins::error::ErrorKind, js_string, run_test_actions, JsValue, TestAction}; +use crate::{js_string, run_test_actions, JsNativeErrorKind, JsValue, TestAction}; #[test] fn length() { @@ -104,7 +104,7 @@ fn repeat() { fn repeat_throws_when_count_is_negative() { run_test_actions([TestAction::assert_native_error( "'x'.repeat(-1)", - ErrorKind::Range, + JsNativeErrorKind::Range, "repeat count must be a positive finite number \ that doesn't overflow the maximum string length (2^32 - 1)", )]); @@ -114,7 +114,7 @@ fn repeat_throws_when_count_is_negative() { fn repeat_throws_when_count_is_infinity() { run_test_actions([TestAction::assert_native_error( "'x'.repeat(Infinity)", - ErrorKind::Range, + JsNativeErrorKind::Range, "repeat count must be a positive finite number \ that doesn't overflow the maximum string length (2^32 - 1)", )]); @@ -124,7 +124,7 @@ fn repeat_throws_when_count_is_infinity() { fn repeat_throws_when_count_overflows_max_length() { run_test_actions([TestAction::assert_native_error( "'x'.repeat(2 ** 64)", - ErrorKind::Range, + JsNativeErrorKind::Range, "repeat count must be a positive finite number \ that doesn't overflow the maximum string length (2^32 - 1)", )]); @@ -247,7 +247,7 @@ fn starts_with() { fn starts_with_with_regex_arg() { run_test_actions([TestAction::assert_native_error( "'Saturday night'.startsWith(/Saturday/)", - ErrorKind::Type, + JsNativeErrorKind::Type, "First argument to String.prototype.startsWith must not be a regular expression", )]); } @@ -273,7 +273,7 @@ fn ends_with() { fn ends_with_with_regex_arg() { run_test_actions([TestAction::assert_native_error( "'Saturday night'.endsWith(/night/)", - ErrorKind::Type, + JsNativeErrorKind::Type, "First argument to String.prototype.endsWith must not be a regular expression", )]); } @@ -299,7 +299,7 @@ fn includes() { fn includes_with_regex_arg() { run_test_actions([TestAction::assert_native_error( "'Saturday night'.includes(/day/)", - ErrorKind::Type, + JsNativeErrorKind::Type, "First argument to String.prototype.includes must not be a regular expression", )]); } @@ -589,7 +589,7 @@ fn split_with_symbol_split_method() { sep[Symbol.split] = 10; 'hello'.split(sep, 10); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "value returned for property of object is not a function", ), ]); @@ -880,32 +880,32 @@ fn from_code_point() { TestAction::assert_eq("String.fromCodePoint(9731, 9733, 9842, 0x4F60)", "☃★♲你"), TestAction::assert_native_error( "String.fromCodePoint('_')", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `NaN` is not an integer", ), TestAction::assert_native_error( "String.fromCodePoint(Infinity)", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `inf` is not an integer", ), TestAction::assert_native_error( "String.fromCodePoint(-1)", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `-1` outside of Unicode range", ), TestAction::assert_native_error( "String.fromCodePoint(3.14)", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `3.14` is not an integer", ), TestAction::assert_native_error( "String.fromCodePoint(3e-2)", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `0.03` is not an integer", ), TestAction::assert_native_error( "String.fromCodePoint(NaN)", - ErrorKind::Range, + JsNativeErrorKind::Range, "codepoint `NaN` is not an integer", ), ]); diff --git a/boa_engine/src/bytecompiler/statement/loop.rs b/boa_engine/src/bytecompiler/statement/loop.rs index 358b4d7db1..bee7402f32 100644 --- a/boa_engine/src/bytecompiler/statement/loop.rs +++ b/boa_engine/src/bytecompiler/statement/loop.rs @@ -56,6 +56,7 @@ impl ByteCompiler<'_, '_> { let (continue_start, continue_exit) = self.emit_opcode_with_two_operands(Opcode::LoopContinue); + self.patch_jump_with_target(loop_start, start_address); self.patch_jump_with_target(continue_start, start_address); @@ -345,6 +346,7 @@ impl ByteCompiler<'_, '_> { let start_address = self.next_opcode_location(); let (continue_start, continue_exit) = self.emit_opcode_with_two_operands(Opcode::LoopContinue); + self.push_loop_control_info(label, start_address); self.patch_jump_with_target(loop_start, start_address); self.patch_jump_with_target(continue_start, start_address); diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 2d3e1ebf64..67d5f9bdd7 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -40,6 +40,8 @@ use boa_interner::{Interner, Sym}; use boa_parser::{Error as ParseError, Parser}; use boa_profiler::Profiler; +use crate::vm::RuntimeLimits; + /// ECMAScript context. It is the primary way to interact with the runtime. /// /// `Context`s constructed in a thread share the same runtime, therefore it @@ -548,30 +550,36 @@ impl<'host> Context<'host> { /// Set the value of trace on the context #[cfg(feature = "trace")] + #[inline] pub fn set_trace(&mut self, trace: bool) { self.vm.trace = trace; } /// Get optimizer options. + #[inline] pub const fn optimizer_options(&self) -> OptimizerOptions { self.optimizer_options } /// Enable or disable optimizations + #[inline] pub fn set_optimizer_options(&mut self, optimizer_options: OptimizerOptions) { self.optimizer_options = optimizer_options; } /// Changes the strictness mode of the context. + #[inline] pub fn strict(&mut self, strict: bool) { self.strict = strict; } /// Enqueues a [`NativeJob`] on the [`JobQueue`]. + #[inline] pub fn enqueue_job(&mut self, job: NativeJob) { self.job_queue().enqueue_promise_job(job, self); } /// Runs all the jobs in the job queue. + #[inline] pub fn run_jobs(&mut self) { self.job_queue().run_jobs(self); self.clear_kept_objects(); @@ -585,16 +593,19 @@ impl<'host> Context<'host> { /// [clear]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-clear-kept-objects /// [add]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-addtokeptobjects /// [weak]: https://tc39.es/ecma262/multipage/managing-memory.html#sec-weak-ref-objects + #[inline] pub fn clear_kept_objects(&mut self) { self.kept_alive.clear(); } /// Retrieves the current stack trace of the context. + #[inline] pub fn stack_trace(&mut self) -> impl Iterator { self.vm.frames.iter().rev() } /// Replaces the currently active realm with `realm`, and returns the old realm. + #[inline] pub fn enter_realm(&mut self, realm: Realm) -> Realm { self.vm .environments @@ -607,14 +618,34 @@ impl<'host> Context<'host> { } /// Gets the host hooks. + #[inline] pub fn host_hooks(&self) -> MaybeShared<'host, dyn HostHooks> { self.host_hooks.clone() } /// Gets the job queue. + #[inline] pub fn job_queue(&self) -> MaybeShared<'host, dyn JobQueue> { self.job_queue.clone() } + + /// Get the [`RuntimeLimits`]. + #[inline] + pub const fn runtime_limits(&self) -> RuntimeLimits { + self.vm.runtime_limits + } + + /// Set the [`RuntimeLimits`]. + #[inline] + pub fn set_runtime_limits(&mut self, runtime_limits: RuntimeLimits) { + self.vm.runtime_limits = runtime_limits; + } + + /// Get a mutable reference to the [`RuntimeLimits`]. + #[inline] + pub fn runtime_limits_mut(&mut self) -> &mut RuntimeLimits { + &mut self.vm.runtime_limits + } } // ==== Private API ==== diff --git a/boa_engine/src/environments/tests.rs b/boa_engine/src/environments/tests.rs index 5baee075ab..e900eb4b47 100644 --- a/boa_engine/src/environments/tests.rs +++ b/boa_engine/src/environments/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; use indoc::indoc; #[test] @@ -10,7 +10,7 @@ fn let_is_block_scoped() { } bar; "#}, - ErrorKind::Reference, + JsNativeErrorKind::Reference, "bar is not defined", )]); } @@ -24,7 +24,7 @@ fn const_is_block_scoped() { } bar; "#}, - ErrorKind::Reference, + JsNativeErrorKind::Reference, "bar is not defined", )]); } @@ -51,7 +51,7 @@ fn functions_use_declaration_scope() { foo(); } "#}, - ErrorKind::Reference, + JsNativeErrorKind::Reference, "bar is not defined", )]); } diff --git a/boa_engine/src/error.rs b/boa_engine/src/error.rs index d85e40674f..ce5899166d 100644 --- a/boa_engine/src/error.rs +++ b/boa_engine/src/error.rs @@ -44,7 +44,7 @@ use thiserror::Error; /// let kind = &native_error.as_native().unwrap().kind; /// assert!(matches!(kind, JsNativeErrorKind::Type)); /// ``` -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)] pub struct JsError { inner: Repr, } @@ -59,7 +59,7 @@ pub struct JsError { /// This should never be used outside of this module. If that's not the case, /// you should add methods to either `JsError` or `JsNativeError` to /// represent that special use case. -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)] enum Repr { Native(JsNativeError), Opaque(JsValue), @@ -417,7 +417,7 @@ impl std::fmt::Display for JsError { /// /// assert_eq!(native_error.message(), "cannot decode uri"); /// ``` -#[derive(Clone, Trace, Finalize, Error)] +#[derive(Clone, Trace, Finalize, Error, PartialEq, Eq)] #[error("{kind}: {message}")] pub struct JsNativeError { /// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.) @@ -468,10 +468,17 @@ impl JsNativeError { /// )); /// ``` #[must_use] + #[inline] pub fn aggregate(errors: Vec) -> Self { Self::new(JsNativeErrorKind::Aggregate(errors), Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Aggregate`]. + #[inline] + pub const fn is_aggregate(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Aggregate(_)) + } + /// Creates a new `JsNativeError` of kind `Error`, with empty `message` and undefined `cause`. /// /// # Examples @@ -483,10 +490,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Error)); /// ``` #[must_use] + #[inline] pub fn error() -> Self { Self::new(JsNativeErrorKind::Error, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Error`]. + #[inline] + pub const fn is_error(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Error) + } + /// Creates a new `JsNativeError` of kind `EvalError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -498,10 +512,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Eval)); /// ``` #[must_use] + #[inline] pub fn eval() -> Self { Self::new(JsNativeErrorKind::Eval, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Eval`]. + #[inline] + pub const fn is_eval(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Eval) + } + /// Creates a new `JsNativeError` of kind `RangeError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -513,10 +534,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Range)); /// ``` #[must_use] + #[inline] pub fn range() -> Self { Self::new(JsNativeErrorKind::Range, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Range`]. + #[inline] + pub const fn is_range(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Range) + } + /// Creates a new `JsNativeError` of kind `ReferenceError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -528,10 +556,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Reference)); /// ``` #[must_use] + #[inline] pub fn reference() -> Self { Self::new(JsNativeErrorKind::Reference, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Reference`]. + #[inline] + pub const fn is_reference(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Reference) + } + /// Creates a new `JsNativeError` of kind `SyntaxError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -543,10 +578,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Syntax)); /// ``` #[must_use] + #[inline] pub fn syntax() -> Self { Self::new(JsNativeErrorKind::Syntax, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Syntax`]. + #[inline] + pub const fn is_syntax(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Syntax) + } + /// Creates a new `JsNativeError` of kind `TypeError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -558,10 +600,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Type)); /// ``` #[must_use] + #[inline] pub fn typ() -> Self { Self::new(JsNativeErrorKind::Type, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Type`]. + #[inline] + pub const fn is_type(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Type) + } + /// Creates a new `JsNativeError` of kind `UriError`, with empty `message` and undefined `cause`. /// /// # Examples @@ -573,10 +622,17 @@ impl JsNativeError { /// assert!(matches!(error.kind, JsNativeErrorKind::Uri)); /// ``` #[must_use] + #[inline] pub fn uri() -> Self { Self::new(JsNativeErrorKind::Uri, Box::default(), None) } + /// Check if it's a [`JsNativeErrorKind::Uri`]. + #[inline] + pub const fn is_uri(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::Uri) + } + /// Creates a new `JsNativeError` that indicates that the context hit its execution limit. This /// is only used in a fuzzing context. #[cfg(feature = "fuzz")] @@ -589,6 +645,26 @@ impl JsNativeError { ) } + /// Check if it's a [`JsNativeErrorKind::NoInstructionsRemain`]. + #[inline] + #[cfg(feature = "fuzz")] + pub const fn is_no_instructions_remain(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::NoInstructionsRemain) + } + + /// Creates a new `JsNativeError` that indicates that the context exceeded the runtime limits. + #[must_use] + #[inline] + pub fn runtime_limit() -> Self { + Self::new(JsNativeErrorKind::RuntimeLimit, Box::default(), None) + } + + /// Check if it's a [`JsNativeErrorKind::RuntimeLimit`]. + #[inline] + pub const fn is_runtime_limit(&self) -> bool { + matches!(self.kind, JsNativeErrorKind::RuntimeLimit) + } + /// Sets the message of this error. /// /// # Examples @@ -600,6 +676,7 @@ impl JsNativeError { /// assert_eq!(error.message(), "number too large"); /// ``` #[must_use] + #[inline] pub fn with_message(mut self, message: S) -> Self where S: Into>, @@ -620,6 +697,7 @@ impl JsNativeError { /// assert!(error.cause().unwrap().as_native().is_some()); /// ``` #[must_use] + #[inline] pub fn with_cause(mut self, cause: V) -> Self where V: Into, @@ -644,6 +722,7 @@ impl JsNativeError { /// assert_eq!(error.message(), "number too large"); /// ``` #[must_use] + #[inline] pub const fn message(&self) -> &str { &self.message } @@ -665,6 +744,7 @@ impl JsNativeError { /// assert!(error.cause().unwrap().as_native().is_some()); /// ``` #[must_use] + #[inline] pub fn cause(&self) -> Option<&JsError> { self.cause.as_deref() } @@ -683,6 +763,11 @@ impl JsNativeError { /// assert!(error_obj.borrow().is_error()); /// assert_eq!(error_obj.get("message", context).unwrap(), "error!".into()) /// ``` + /// + /// # Panics + /// + /// If converting a [`JsNativeErrorKind::RuntimeLimit`] to an opaque object. + #[inline] pub fn to_opaque(&self, context: &mut Context<'_>) -> JsObject { let Self { kind, @@ -717,6 +802,9 @@ impl JsNativeError { "The NoInstructionsRemain native error cannot be converted to an opaque type." ) } + JsNativeErrorKind::RuntimeLimit => { + panic!("The RuntimeLimit native error cannot be converted to an opaque type.") + } }; let o = JsObject::from_proto_and_data_with_shared_shape( @@ -776,7 +864,7 @@ impl From for JsNativeError { /// /// [spec]: https://tc39.es/ecma262/#sec-error-objects /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)] #[non_exhaustive] pub enum JsNativeErrorKind { /// A collection of errors wrapped in a single error. @@ -855,10 +943,14 @@ pub enum JsNativeErrorKind { /// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI /// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI Uri, + /// Error thrown when no instructions remain. Only used in a fuzzing context; not a valid JS /// error variant. #[cfg(feature = "fuzz")] NoInstructionsRemain, + + /// Error thrown when a runtime limit is exceeded. It's not a valid JS error variant. + RuntimeLimit, } impl PartialEq for JsNativeErrorKind { @@ -888,6 +980,7 @@ impl std::fmt::Display for JsNativeErrorKind { Self::Syntax => "SyntaxError", Self::Type => "TypeError", Self::Uri => "UriError", + Self::RuntimeLimit => "RuntimeLimit", #[cfg(feature = "fuzz")] Self::NoInstructionsRemain => "NoInstructionsRemain", } diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 8b37dc5aaa..985da500de 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -257,7 +257,7 @@ enum Inner { }, AssertNativeError { source: Cow<'static, str>, - kind: builtins::error::ErrorKind, + kind: JsNativeErrorKind, message: &'static str, }, AssertContext { @@ -328,7 +328,7 @@ impl TestAction { /// Asserts that evaluating `source` throws a native error of `kind` and `message`. fn assert_native_error( source: impl Into>, - kind: builtins::error::ErrorKind, + kind: JsNativeErrorKind, message: &'static str, ) -> Self { Self(Inner::AssertNativeError { diff --git a/boa_engine/src/object/tests.rs b/boa_engine/src/object/tests.rs index 5b59092652..9113845f23 100644 --- a/boa_engine/src/object/tests.rs +++ b/boa_engine/src/object/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; use indoc::indoc; #[test] @@ -9,7 +9,7 @@ fn ordinary_has_instance_nonobject_prototype() { C.prototype = 1 String instanceof C "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "function has non-object prototype in instanceof check", )]); } diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 22b4d5c4d6..3967f32d38 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -24,6 +24,14 @@ pub struct Realm { inner: Gc, } +impl Eq for Realm {} + +impl PartialEq for Realm { + fn eq(&self, other: &Self) -> bool { + Gc::ptr_eq(&self.inner, &other.inner) + } +} + impl fmt::Debug for Realm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Realm") @@ -35,12 +43,6 @@ impl fmt::Debug for Realm { } } -impl PartialEq for Realm { - fn eq(&self, other: &Self) -> bool { - std::ptr::eq(&*self.inner, &*other.inner) - } -} - #[derive(Trace, Finalize)] struct Inner { intrinsics: Intrinsics, diff --git a/boa_engine/src/tests/control_flow/loops.rs b/boa_engine/src/tests/control_flow/loops.rs index 1a91fd6f78..683ac7a707 100644 --- a/boa_engine/src/tests/control_flow/loops.rs +++ b/boa_engine/src/tests/control_flow/loops.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; use indoc::indoc; #[test] @@ -125,7 +125,7 @@ fn for_loop_iteration_variable_does_not_leak() { for (let i = 0;false;) {} i "#}, - ErrorKind::Reference, + JsNativeErrorKind::Reference, "i is not defined", )]); } @@ -138,7 +138,7 @@ fn test_invalid_break_target() { break nonexistent; } "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "undefined break target: nonexistent at line 1, col 1", )]); } @@ -625,7 +625,7 @@ fn for_of_loop_let() { } "#}), TestAction::assert_eq("result", 3), - TestAction::assert_native_error("i", ErrorKind::Reference, "i is not defined"), + TestAction::assert_native_error("i", JsNativeErrorKind::Reference, "i is not defined"), ]); } @@ -639,7 +639,7 @@ fn for_of_loop_const() { } "#}), TestAction::assert_eq("result", 3), - TestAction::assert_native_error("i", ErrorKind::Reference, "i is not defined"), + TestAction::assert_native_error("i", JsNativeErrorKind::Reference, "i is not defined"), ]); } diff --git a/boa_engine/src/tests/control_flow/mod.rs b/boa_engine/src/tests/control_flow/mod.rs index 738f7bb0d2..8078bf49bf 100644 --- a/boa_engine/src/tests/control_flow/mod.rs +++ b/boa_engine/src/tests/control_flow/mod.rs @@ -1,13 +1,13 @@ use indoc::indoc; mod loops; -use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, TestAction}; #[test] fn test_invalid_break() { run_test_actions([TestAction::assert_native_error( "break;", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "illegal break statement at line 1, col 1", )]); } @@ -20,7 +20,7 @@ fn test_invalid_continue_target() { continue nonexistent; } "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "undefined continue target: nonexistent at line 1, col 1", )]); } @@ -29,7 +29,7 @@ fn test_invalid_continue_target() { fn test_invalid_continue() { run_test_actions([TestAction::assert_native_error( "continue;", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "illegal continue statement at line 1, col 1", )]); } diff --git a/boa_engine/src/tests/function.rs b/boa_engine/src/tests/function.rs index 8fab9cbb66..5fe53613a0 100644 --- a/boa_engine/src/tests/function.rs +++ b/boa_engine/src/tests/function.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -87,7 +87,7 @@ fn should_set_this_value() { fn should_type_error_when_new_is_not_constructor() { run_test_actions([TestAction::assert_native_error( "new ''()", - ErrorKind::Type, + JsNativeErrorKind::Type, "not a constructor", )]); } @@ -125,9 +125,13 @@ fn not_a_function() { let a = {}; let b = true; "#}), - TestAction::assert_native_error("a()", ErrorKind::Type, "not a callable function"), - TestAction::assert_native_error("a.a()", ErrorKind::Type, "not a callable function"), - TestAction::assert_native_error("b()", ErrorKind::Type, "not a callable function"), + TestAction::assert_native_error("a()", JsNativeErrorKind::Type, "not a callable function"), + TestAction::assert_native_error( + "a.a()", + JsNativeErrorKind::Type, + "not a callable function", + ), + TestAction::assert_native_error("b()", JsNativeErrorKind::Type, "not a callable function"), ]); } @@ -140,7 +144,7 @@ fn strict_mode_dup_func_parameters() { 'use strict'; function f(a, b, b) {} "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Duplicate parameter name not allowed in this context at line 2, col 12", )]); } diff --git a/boa_engine/src/tests/mod.rs b/boa_engine/src/tests/mod.rs index c59dabec59..667eee3a38 100644 --- a/boa_engine/src/tests/mod.rs +++ b/boa_engine/src/tests/mod.rs @@ -7,7 +7,7 @@ mod operators; mod promise; mod spread; -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; #[test] fn length_correct_value_on_string_literal() { @@ -40,7 +40,7 @@ fn empty_var_decl_undefined() { fn identifier_on_global_object_undefined() { run_test_actions([TestAction::assert_native_error( "bar;", - ErrorKind::Reference, + JsNativeErrorKind::Reference, "bar is not defined", )]); } @@ -372,7 +372,7 @@ fn undefined_constant() { fn identifier_op() { run_test_actions([TestAction::assert_native_error( "break = 1", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, r#"expected token 'identifier', got '=' in identifier parsing at line 1, col 7"#, )]); } @@ -386,7 +386,7 @@ fn strict_mode_octal() { 'use strict'; var n = 023; "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "implicit octal literals are not allowed in strict mode at line 2, col 9", )]); } @@ -404,7 +404,7 @@ fn strict_mode_with() { } } "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "with statement not allowed in strict mode at line 3, col 5", )]); } @@ -429,7 +429,11 @@ fn strict_mode_reserved_name() { ]; run_test_actions(cases.into_iter().map(|(case, msg)| { - TestAction::assert_native_error(format!("'use strict'; {case}"), ErrorKind::Syntax, msg) + TestAction::assert_native_error( + format!("'use strict'; {case}"), + JsNativeErrorKind::Syntax, + msg, + ) })); } diff --git a/boa_engine/src/tests/operators.rs b/boa_engine/src/tests/operators.rs index 5c89157d6a..c1280e1457 100644 --- a/boa_engine/src/tests/operators.rs +++ b/boa_engine/src/tests/operators.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -143,22 +143,22 @@ fn invalid_unary_access() { run_test_actions([ TestAction::assert_native_error( "++[]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 1", ), TestAction::assert_native_error( "[]++", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 3", ), TestAction::assert_native_error( "--[]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 1", ), TestAction::assert_native_error( "[]--", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 3", ), ]); @@ -170,22 +170,22 @@ fn unary_operations_on_this() { run_test_actions([ TestAction::assert_native_error( "++this", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 1", ), TestAction::assert_native_error( "--this", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 1", ), TestAction::assert_native_error( "this++", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 5", ), TestAction::assert_native_error( "this--", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 5", ), ]); @@ -305,7 +305,7 @@ fn assignment_to_non_assignable() { .map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 3", ) }), @@ -330,7 +330,7 @@ fn assignment_to_non_assignable_ctd() { .map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 13", ) }), @@ -344,7 +344,7 @@ fn multicharacter_assignment_to_non_assignable() { run_test_actions(["3 **= 5", "3 <<= 5", "3 >>= 5"].into_iter().map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 3", ) })); @@ -358,7 +358,7 @@ fn multicharacter_assignment_to_non_assignable_ctd() { .map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 13", ) }), @@ -373,7 +373,7 @@ fn multicharacter_bitwise_assignment_to_non_assignable() { .map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 3", ) }), @@ -393,7 +393,7 @@ fn multicharacter_bitwise_assignment_to_non_assignable_ctd() { .map(|src| { TestAction::assert_native_error( src, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 13", ) }), @@ -405,22 +405,22 @@ fn assign_to_array_decl() { run_test_actions([ TestAction::assert_native_error( "[1] = [2]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 5", ), TestAction::assert_native_error( "[3, 5] = [7, 8]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 8", ), TestAction::assert_native_error( "[6, 8] = [2]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 8", ), TestAction::assert_native_error( "[6] = [2, 9]", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "Invalid left-hand side in assignment at line 1, col 5", ), ]); @@ -430,7 +430,7 @@ fn assign_to_array_decl() { fn assign_to_object_decl() { run_test_actions([TestAction::assert_native_error( "{a: 3} = {a: 5};", - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "unexpected token '=', primary expression at line 1, col 8", )]); } @@ -439,7 +439,7 @@ fn assign_to_object_decl() { fn assignmentoperator_lhs_not_defined() { run_test_actions([TestAction::assert_native_error( "a += 5", - ErrorKind::Reference, + JsNativeErrorKind::Reference, "a is not defined", )]); } @@ -448,7 +448,7 @@ fn assignmentoperator_lhs_not_defined() { fn assignmentoperator_rhs_throws_error() { run_test_actions([TestAction::assert_native_error( "let a; a += b", - ErrorKind::Reference, + JsNativeErrorKind::Reference, "b is not defined", )]); } @@ -457,7 +457,7 @@ fn assignmentoperator_rhs_throws_error() { fn instanceofoperator_rhs_not_object() { run_test_actions([TestAction::assert_native_error( "let s = new String(); s instanceof 1", - ErrorKind::Type, + JsNativeErrorKind::Type, "right-hand side of 'instanceof' should be an object, got `number`", )]); } @@ -466,7 +466,7 @@ fn instanceofoperator_rhs_not_object() { fn instanceofoperator_rhs_not_callable() { run_test_actions([TestAction::assert_native_error( "let s = new String(); s instanceof {}", - ErrorKind::Type, + JsNativeErrorKind::Type, "right-hand side of 'instanceof' is not callable", )]); } @@ -504,7 +504,7 @@ fn delete_variable_in_strict() { let x = 10; delete x; "#}, - ErrorKind::Syntax, + JsNativeErrorKind::Syntax, "cannot delete variables in strict mode at line 3, col 1", )]); } @@ -513,7 +513,7 @@ fn delete_variable_in_strict() { fn delete_non_configurable() { run_test_actions([TestAction::assert_native_error( "'use strict'; delete Boolean.prototype", - ErrorKind::Type, + JsNativeErrorKind::Type, "Cannot delete property", )]); } @@ -528,7 +528,7 @@ fn delete_non_configurable_in_function() { } t() "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Cannot delete property", )]); } @@ -557,7 +557,7 @@ fn delete_in_function_global_strict() { } a(); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Cannot delete property", )]); } @@ -591,7 +591,7 @@ fn delete_in_strict_function_returned() { } a()(); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "Cannot delete property", )]); } @@ -633,7 +633,7 @@ mod in_operator { fn should_type_error_when_rhs_not_object() { run_test_actions([TestAction::assert_native_error( "'fail' in undefined", - ErrorKind::Type, + JsNativeErrorKind::Type, "right-hand side of 'in' should be an object, got `undefined`", )]); } diff --git a/boa_engine/src/tests/spread.rs b/boa_engine/src/tests/spread.rs index e5f01edb86..05ad16fc95 100644 --- a/boa_engine/src/tests/spread.rs +++ b/boa_engine/src/tests/spread.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -97,7 +97,7 @@ fn spread_getters_in_object() { var a = { x: 42 }; var aWithXGetter = { ...a, ... { get x() { throw new Error('not thrown yet') } } }; "#}, - ErrorKind::Error, + JsNativeErrorKind::Error, "not thrown yet", )]); } diff --git a/boa_engine/src/value/tests.rs b/boa_engine/src/value/tests.rs index 21d7637eb3..d0d558e4a0 100644 --- a/boa_engine/src/value/tests.rs +++ b/boa_engine/src/value/tests.rs @@ -700,7 +700,7 @@ fn to_bigint() { /// Relevant mitigation for these are in `JsObject::ordinary_to_primitive` and /// `JsObject::to_json` mod cyclic_conversions { - use crate::builtins::error::ErrorKind; + use crate::JsNativeErrorKind; use super::*; @@ -712,7 +712,7 @@ mod cyclic_conversions { a[0] = a; JSON.stringify(a) "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "cyclic object value", )]); } diff --git a/boa_engine/src/vm/call_frame/env_stack.rs b/boa_engine/src/vm/call_frame/env_stack.rs index 5d4c8d4597..be34ea945a 100644 --- a/boa_engine/src/vm/call_frame/env_stack.rs +++ b/boa_engine/src/vm/call_frame/env_stack.rs @@ -3,7 +3,10 @@ #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) enum EnvEntryKind { Global, - Loop, + Loop { + /// This is used to keep track of how many iterations a loop has done. + iteration_count: u64, + }, Try, Catch, Finally, @@ -49,8 +52,9 @@ impl EnvStackEntry { } /// Returns calling `EnvStackEntry` with `kind` field of `Loop`. - pub(crate) const fn with_loop_flag(mut self) -> Self { - self.kind = EnvEntryKind::Loop; + /// And the loop iteration set to zero. + pub(crate) const fn with_loop_flag(mut self, iteration_count: u64) -> Self { + self.kind = EnvEntryKind::Loop { iteration_count }; self } @@ -95,8 +99,15 @@ impl EnvStackEntry { } /// Returns true if an `EnvStackEntry` is a loop - pub(crate) fn is_loop_env(&self) -> bool { - self.kind == EnvEntryKind::Loop + pub(crate) const fn is_loop_env(&self) -> bool { + matches!(self.kind, EnvEntryKind::Loop { .. }) + } + + pub(crate) const fn as_loop_iteration_count(self) -> Option { + if let EnvEntryKind::Loop { iteration_count } = self.kind { + return Some(iteration_count); + } + None } /// Returns true if an `EnvStackEntry` is a try block diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index 24b17a1421..3be31ed666 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -108,7 +108,6 @@ impl CodeBlock { EdgeStyle::Line, ); } - Opcode::JumpIfFalse | Opcode::JumpIfTrue | Opcode::JumpIfNotUndefined diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 310d87d090..239be0ec4d 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -4,14 +4,15 @@ //! This module will provide an instruction set for the AST to use, various traits, //! plus an interpreter to execute those instructions +#[cfg(feature = "fuzz")] +use crate::JsNativeError; use crate::{ builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack}, vm::code_block::Readable, Context, JsError, JsObject, JsResult, JsValue, }; -#[cfg(feature = "fuzz")] -use crate::{JsNativeError, JsNativeErrorKind}; + use boa_gc::Gc; use boa_profiler::Profiler; use std::{convert::TryInto, mem::size_of}; @@ -26,9 +27,12 @@ mod code_block; mod completion_record; mod opcode; +mod runtime_limits; + #[cfg(feature = "flowgraph")] pub mod flowgraph; +pub use runtime_limits::RuntimeLimits; pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode}; pub(crate) use { @@ -52,7 +56,7 @@ pub struct Vm { pub(crate) environments: DeclarativeEnvironmentStack, #[cfg(feature = "trace")] pub(crate) trace: bool, - pub(crate) stack_size_limit: usize, + pub(crate) runtime_limits: RuntimeLimits, pub(crate) active_function: Option, } @@ -66,7 +70,7 @@ impl Vm { err: None, #[cfg(feature = "trace")] trace: false, - stack_size_limit: 1024, + runtime_limits: RuntimeLimits::default(), active_function: None, } } @@ -268,14 +272,22 @@ impl Context<'_> { if let Some(native_error) = err.as_native() { // If we hit the execution step limit, bubble up the error to the // (Rust) caller instead of trying to handle as an exception. - if matches!(native_error.kind, JsNativeErrorKind::NoInstructionsRemain) - { + if native_error.is_no_instructions_remain() { self.vm.err = Some(err); break CompletionType::Throw; } } } + if let Some(native_error) = err.as_native() { + // If we hit the execution step limit, bubble up the error to the + // (Rust) caller instead of trying to handle as an exception. + if native_error.is_runtime_limit() { + self.vm.err = Some(err); + break CompletionType::Throw; + } + } + self.vm.err = Some(err); // If this frame has not evaluated the throw as an AbruptCompletion, then evaluate it diff --git a/boa_engine/src/vm/opcode/call/mod.rs b/boa_engine/src/vm/opcode/call/mod.rs index 8cb582af5a..6f59fcb301 100644 --- a/boa_engine/src/vm/opcode/call/mod.rs +++ b/boa_engine/src/vm/opcode/call/mod.rs @@ -17,8 +17,8 @@ impl Operation for CallEval { const INSTRUCTION: &'static str = "INST - CallEval"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } @@ -77,8 +77,8 @@ impl Operation for CallEvalSpread { const INSTRUCTION: &'static str = "INST - CallEvalSpread"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } @@ -143,8 +143,8 @@ impl Operation for Call { const INSTRUCTION: &'static str = "INST - Call"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } @@ -182,8 +182,8 @@ impl Operation for CallSpread { const INSTRUCTION: &'static str = "INST - CallSpread"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } diff --git a/boa_engine/src/vm/opcode/iteration/loop_ops.rs b/boa_engine/src/vm/opcode/iteration/loop_ops.rs index 3e14dca770..3b586aeb24 100644 --- a/boa_engine/src/vm/opcode/iteration/loop_ops.rs +++ b/boa_engine/src/vm/opcode/iteration/loop_ops.rs @@ -1,3 +1,4 @@ +use crate::JsNativeError; use crate::{ vm::{call_frame::EnvStackEntry, opcode::Operation, CompletionType}, Context, JsResult, @@ -18,15 +19,29 @@ impl Operation for LoopStart { let start = context.vm.read::(); let exit = context.vm.read::(); - context - .vm - .frame_mut() - .env_stack - .push(EnvStackEntry::new(start, exit).with_loop_flag()); + // Create and push loop evironment entry. + let entry = EnvStackEntry::new(start, exit).with_loop_flag(1); + context.vm.frame_mut().env_stack.push(entry); Ok(CompletionType::Normal) } } +/// This is a helper function used to clean the loop environment created by the +/// [`LoopStart`] and [`LoopContinue`] opcodes. +fn cleanup_loop_environment(context: &mut Context<'_>) { + let mut envs_to_pop = 0_usize; + while let Some(env_entry) = context.vm.frame_mut().env_stack.pop() { + envs_to_pop += env_entry.env_num(); + + if env_entry.is_loop_env() { + break; + } + } + + let env_truncation_len = context.vm.environments.len().saturating_sub(envs_to_pop); + context.vm.environments.truncate(env_truncation_len); +} + /// `LoopContinue` implements the Opcode Operation for `Opcode::LoopContinue`. /// /// Operation: @@ -42,6 +57,8 @@ impl Operation for LoopContinue { let start = context.vm.read::(); let exit = context.vm.read::(); + let mut iteration_count = 0; + // 1. Clean up the previous environment. if let Some(entry) = context .vm @@ -57,15 +74,28 @@ impl Operation for LoopContinue { .saturating_sub(entry.env_num()); context.vm.environments.truncate(env_truncation_len); - context.vm.frame_mut().env_stack.pop(); + // Pop loop environment and get it's iteration count. + let previous_entry = context.vm.frame_mut().env_stack.pop(); + if let Some(previous_iteration_count) = + previous_entry.and_then(EnvStackEntry::as_loop_iteration_count) + { + iteration_count = previous_iteration_count.wrapping_add(1); + + let max = context.vm.runtime_limits.loop_iteration_limit(); + if previous_iteration_count > max { + cleanup_loop_environment(context); + + return Err(JsNativeError::runtime_limit() + .with_message(format!("max loop iteration limit {max} exceeded")) + .into()); + } + } } // 2. Push a new clean EnvStack. - context - .vm - .frame_mut() - .env_stack - .push(EnvStackEntry::new(start, exit).with_loop_flag()); + let entry = EnvStackEntry::new(start, exit).with_loop_flag(iteration_count); + + context.vm.frame_mut().env_stack.push(entry); Ok(CompletionType::Normal) } @@ -83,18 +113,7 @@ impl Operation for LoopEnd { const INSTRUCTION: &'static str = "INST - LoopEnd"; fn execute(context: &mut Context<'_>) -> JsResult { - let mut envs_to_pop = 0_usize; - while let Some(env_entry) = context.vm.frame_mut().env_stack.pop() { - envs_to_pop += env_entry.env_num(); - - if env_entry.is_loop_env() { - break; - } - } - - let env_truncation_len = context.vm.environments.len().saturating_sub(envs_to_pop); - context.vm.environments.truncate(env_truncation_len); - + cleanup_loop_environment(context); Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/new/mod.rs b/boa_engine/src/vm/opcode/new/mod.rs index 35626b7a8b..80c58ea00a 100644 --- a/boa_engine/src/vm/opcode/new/mod.rs +++ b/boa_engine/src/vm/opcode/new/mod.rs @@ -16,8 +16,8 @@ impl Operation for New { const INSTRUCTION: &'static str = "INST - New"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } @@ -55,8 +55,8 @@ impl Operation for NewSpread { const INSTRUCTION: &'static str = "INST - NewSpread"; fn execute(context: &mut Context<'_>) -> JsResult { - if context.vm.stack_size_limit <= context.vm.stack.len() { - return Err(JsNativeError::range() + if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() { + return Err(JsNativeError::runtime_limit() .with_message("Maximum call stack size exceeded") .into()); } diff --git a/boa_engine/src/vm/runtime_limits.rs b/boa_engine/src/vm/runtime_limits.rs new file mode 100644 index 0000000000..00083b697a --- /dev/null +++ b/boa_engine/src/vm/runtime_limits.rs @@ -0,0 +1,61 @@ +/// Represents the limits of different runtime operations. +#[derive(Debug, Clone, Copy)] +pub struct RuntimeLimits { + /// Max stack size before an error is thrown. + stack_size_limit: usize, + + /// Max loop iterations before an error is thrown. + loop_iteration_limit: u64, +} + +impl Default for RuntimeLimits { + #[inline] + fn default() -> Self { + Self { + loop_iteration_limit: u64::MAX, + stack_size_limit: 1024, + } + } +} + +impl RuntimeLimits { + /// Return the loop iteration limit. + /// + /// If the limit is exceeded in a loop it will throw and errror. + /// + /// The limit value [`u64::MAX`] means that there is no limit. + #[inline] + #[must_use] + pub const fn loop_iteration_limit(&self) -> u64 { + self.loop_iteration_limit + } + + /// Set the loop iteration limit. + /// + /// If the limit is exceeded in a loop it will throw and errror. + /// + /// Setting the limit to [`u64::MAX`] means that there is no limit. + #[inline] + pub fn set_loop_iteration_limit(&mut self, value: u64) { + self.loop_iteration_limit = value; + } + + /// Disable loop iteration limit. + #[inline] + pub fn disable_loop_iteration_limit(&mut self) { + self.loop_iteration_limit = u64::MAX; + } + + /// Get max stack size. + #[inline] + #[must_use] + pub const fn stack_size_limit(&self) -> usize { + self.stack_size_limit + } + + /// Set max stack size before an error is thrown. + #[inline] + pub fn set_stack_size_limit(&mut self, value: usize) { + self.stack_size_limit = value; + } +} diff --git a/boa_engine/src/vm/tests.rs b/boa_engine/src/vm/tests.rs index 929e3d9a82..9460ae2dce 100644 --- a/boa_engine/src/vm/tests.rs +++ b/boa_engine/src/vm/tests.rs @@ -1,4 +1,4 @@ -use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; +use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction}; use indoc::indoc; #[test] @@ -190,7 +190,7 @@ fn super_call_constructor_null() { } new A(); "#}, - ErrorKind::Type, + JsNativeErrorKind::Type, "super constructor object must be constructor", )]); } @@ -237,3 +237,38 @@ fn order_of_execution_in_assigment_with_comma_expressions() { "1234", )]); } + +#[test] +fn loop_runtime_limit() { + run_test_actions([ + TestAction::assert_eq( + indoc! {r#" + for (let i = 0; i < 20; ++i) { } + "#}, + JsValue::undefined(), + ), + TestAction::inspect_context(|context| { + context.runtime_limits_mut().set_loop_iteration_limit(10); + }), + TestAction::assert_native_error( + indoc! {r#" + for (let i = 0; i < 20; ++i) { } + "#}, + JsNativeErrorKind::RuntimeLimit, + "max loop iteration limit 10 exceeded", + ), + TestAction::assert_eq( + indoc! {r#" + for (let i = 0; i < 10; ++i) { } + "#}, + JsValue::undefined(), + ), + TestAction::assert_native_error( + indoc! {r#" + while (1) { } + "#}, + JsNativeErrorKind::RuntimeLimit, + "max loop iteration limit 10 exceeded", + ), + ]); +} diff --git a/boa_examples/src/bin/runtime_limits.rs b/boa_examples/src/bin/runtime_limits.rs new file mode 100644 index 0000000000..c1e518063c --- /dev/null +++ b/boa_examples/src/bin/runtime_limits.rs @@ -0,0 +1,47 @@ +use boa_engine::{Context, Source}; + +fn main() { + // Create the JavaScript context. + let mut context = Context::default(); + + // Set the context's runtime limit on loops to 10 iterations. + context.runtime_limits_mut().set_loop_iteration_limit(10); + + // The code below iterates 5 times, so no error is thrown. + let result = context.eval_script(Source::from_bytes( + r" + for (let i = 0; i < 5; ++i) { } + ", + )); + result.expect("no error should be thrown"); + + // Here we exceed the limit by 1 iteration and a `RuntimeLimit` error is thrown. + // + // This error cannot be caught in JavaScript it propagates to rust caller. + let result = context.eval_script(Source::from_bytes( + r" + try { + for (let i = 0; i < 11; ++i) { } + } catch (e) { + + } + ", + )); + result.expect_err("should have throw an error"); + + // Preventing an infinity loops + let result = context.eval_script(Source::from_bytes( + r" + while (true) { } + ", + )); + result.expect_err("should have throw an error"); + + // The limit applies to all types of loops. + let result = context.eval_script(Source::from_bytes( + r" + for (let e of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { } + ", + )); + result.expect_err("should have throw an error"); +} diff --git a/docs/boa_object.md b/docs/boa_object.md index fb8fc80da5..184e1d54d7 100644 --- a/docs/boa_object.md +++ b/docs/boa_object.md @@ -173,7 +173,7 @@ $boa.optimizer.constantFolding // true ### Getter & Setter `$boa.optimizer.statistics` -This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. +This is an accessor property on the module, its getter returns `true` if enabled or `false` otherwise. Its setter can be used to enable/disable optimization statistics, which are printed to `stdout`. ```JavaScript @@ -190,7 +190,7 @@ Optimizer { ## Module `$boa.realm` -This modules contains realm utilities to test cross-realm behaviour. +This module contains realm utilities to test cross-realm behaviour. ### `$boa.realm.create` @@ -240,3 +240,18 @@ $boa.shape.same(o1, o2) // true o2.y = 200 $boa.shape.same(o1, o2) // false ``` + +## Module `$boa.limits` + +This module contains utilities for changing runtime limits. + +### Getter & Setter `$boa.limits.loop` + +This is an accessor property on the module, its getter returns the loop iteration limit before an error is thrown. +Its setter can be used to set the loop iteration limit. + +```javascript +$boa.limits.loop = 10; + +while (true) {} // RuntimeLimit: max loop iteration limit 10 exceeded +```