Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement runtime limits for recursion #2904

Merged
merged 4 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion boa_cli/src/debug/limits.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use boa_engine::{
object::{FunctionObjectBuilder, ObjectInitializer},
property::Attribute,
Context, JsArgs, JsObject, JsResult, JsValue, NativeFunction,
Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue, NativeFunction,
};

fn get_loop(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
Expand All @@ -15,6 +15,22 @@ fn set_loop(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResul
Ok(JsValue::undefined())
}

fn get_recursion(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
let max = context.runtime_limits().recursion_limit();
Ok(JsValue::from(max))
}

fn set_recursion(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).to_length(context)?;
let Ok(value) = value.try_into() else {
return Err(
JsNativeError::range().with_message(format!("Argument {value} greater than usize::MAX")).into()
);
};
context.runtime_limits_mut().set_recursion_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")
Expand All @@ -24,12 +40,29 @@ pub(super) fn create_object(context: &mut Context<'_>) -> JsObject {
.name("set loop")
.length(1)
.build();

let get_recursion =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_recursion))
.name("get recursion")
.length(0)
.build();
let set_recursion =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_recursion))
.name("set recursion")
.length(1)
.build();
ObjectInitializer::new(context)
.accessor(
"loop",
Some(get_loop),
Some(set_loop),
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
)
.accessor(
"recursion",
Some(get_recursion),
Some(set_recursion),
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
)
.build()
}
32 changes: 32 additions & 0 deletions boa_engine/src/vm/opcode/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ impl Operation for CallEval {
const INSTRUCTION: &'static str = "INST - CallEval";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down Expand Up @@ -77,6 +85,14 @@ impl Operation for CallEvalSpread {
const INSTRUCTION: &'static str = "INST - CallEvalSpread";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down Expand Up @@ -143,6 +159,14 @@ impl Operation for Call {
const INSTRUCTION: &'static str = "INST - Call";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down Expand Up @@ -182,6 +206,14 @@ impl Operation for CallSpread {
const INSTRUCTION: &'static str = "INST - CallSpread";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down
2 changes: 1 addition & 1 deletion boa_engine/src/vm/opcode/iteration/loop_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl Operation for LoopContinue {
cleanup_loop_environment(context);

return Err(JsNativeError::runtime_limit()
.with_message(format!("max loop iteration limit {max} exceeded"))
.with_message(format!("Maximum loop iteration limit {max} exceeded"))
.into());
}
}
Expand Down
16 changes: 16 additions & 0 deletions boa_engine/src/vm/opcode/new/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ impl Operation for New {
const INSTRUCTION: &'static str = "INST - New";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down Expand Up @@ -55,6 +63,14 @@ impl Operation for NewSpread {
const INSTRUCTION: &'static str = "INST - NewSpread";

fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.runtime_limits.recursion_limit() <= context.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message(format!(
"Maximum recursion limit {} exceeded",
context.vm.runtime_limits.recursion_limit()
))
.into());
}
if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded")
Expand Down
17 changes: 17 additions & 0 deletions boa_engine/src/vm/runtime_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ pub struct RuntimeLimits {

/// Max loop iterations before an error is thrown.
loop_iteration_limit: u64,

/// Max function recursion limit
resursion_limit: usize,
}

impl Default for RuntimeLimits {
#[inline]
fn default() -> Self {
Self {
loop_iteration_limit: u64::MAX,
resursion_limit: 400,
stack_size_limit: 1024,
}
}
Expand Down Expand Up @@ -58,4 +62,17 @@ impl RuntimeLimits {
pub fn set_stack_size_limit(&mut self, value: usize) {
self.stack_size_limit = value;
}

/// Get recursion limit.
#[inline]
#[must_use]
pub const fn recursion_limit(&self) -> usize {
self.resursion_limit
}

/// Set recursion limit before an error is thrown.
#[inline]
pub fn set_recursion_limit(&mut self, value: usize) {
self.resursion_limit = value;
}
}
41 changes: 39 additions & 2 deletions boa_engine/src/vm/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ fn loop_runtime_limit() {
for (let i = 0; i < 20; ++i) { }
"#},
JsNativeErrorKind::RuntimeLimit,
"max loop iteration limit 10 exceeded",
"Maximum loop iteration limit 10 exceeded",
),
TestAction::assert_eq(
indoc! {r#"
Expand All @@ -268,7 +268,44 @@ fn loop_runtime_limit() {
while (1) { }
"#},
JsNativeErrorKind::RuntimeLimit,
"max loop iteration limit 10 exceeded",
"Maximum loop iteration limit 10 exceeded",
),
]);
}

#[test]
fn recursion_runtime_limit() {
run_test_actions([
TestAction::run(indoc! {r#"
function factorial(n) {
if (n == 0) {
return 1;
}

return n * factorial(n - 1);
}
"#}),
TestAction::assert_eq("factorial(8)", JsValue::new(40_320)),
TestAction::assert_eq("factorial(11)", JsValue::new(39_916_800)),
TestAction::inspect_context(|context| {
context.runtime_limits_mut().set_recursion_limit(10);
}),
TestAction::assert_native_error(
"factorial(11)",
JsNativeErrorKind::RuntimeLimit,
"Maximum recursion limit 10 exceeded",
),
TestAction::assert_eq("factorial(8)", JsValue::new(40_320)),
TestAction::assert_native_error(
indoc! {r#"
function x() {
x()
}

x()
"#},
JsNativeErrorKind::RuntimeLimit,
"Maximum recursion limit 10 exceeded",
),
]);
}
47 changes: 42 additions & 5 deletions boa_examples/src/bin/runtime_limits.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use boa_engine::{Context, Source};
use boa_engine::{Context, JsValue, Source};

fn main() {
// Create the JavaScript context.
let mut context = Context::default();

// -----------------------------------------
// Loop Iteration Limit
// -----------------------------------------

// Set the context's runtime limit on loops to 10 iterations.
context.runtime_limits_mut().set_loop_iteration_limit(10);

Expand All @@ -13,7 +17,7 @@ fn main() {
for (let i = 0; i < 5; ++i) { }
",
));
result.expect("no error should be thrown");
assert!(result.is_ok());

// Here we exceed the limit by 1 iteration and a `RuntimeLimit` error is thrown.
//
Expand All @@ -27,21 +31,54 @@ fn main() {
}
",
));
result.expect_err("should have throw an error");
assert!(result.is_err());

// Preventing an infinity loops
let result = context.eval_script(Source::from_bytes(
r"
while (true) { }
",
));
result.expect_err("should have throw an error");
assert!(result.is_err());

// 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");
assert!(result.is_err());

// -----------------------------------------
// Recursion Limit
// -----------------------------------------

// Create and register `factorial` function.
let result = context.eval_script(Source::from_bytes(
r"
function factorial(n) {
if (n == 0) {
return 1;
}

return n * factorial(n - 1);
}
",
));
assert!(result.is_ok());

// Run function before setting the limit and assert that it works.
let result = context.eval_script(Source::from_bytes("factorial(11)"));
assert_eq!(result, Ok(JsValue::new(39_916_800)));

// Setting runtime limit for recustion to 10.
context.runtime_limits_mut().set_recursion_limit(10);

// Run without exceeding recursion limit and assert that it works.
let result = context.eval_script(Source::from_bytes("factorial(8)"));
assert_eq!(result, Ok(JsValue::new(40_320)));

// Run exceeding limit by 1 and assert that it fails.
let result = context.eval_script(Source::from_bytes("factorial(11)"));
assert!(result.is_err());
}
16 changes: 15 additions & 1 deletion docs/boa_object.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,19 @@ 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
while (true) {} // RuntimeLimit: Maximum loop iteration limit 10 exceeded
```

### Getter & Setter `$boa.limits.recursion`

This is an accessor property on the module, its getter returns the recursion limit before an error is thrown.
Its setter can be used to set the recursion limit.

```javascript
$boa.limits.recursion = 100;

function x() {
return x();
}
x(); // RuntimeLimit: Maximum recursion limit 100 exceeded
```
1 change: 0 additions & 1 deletion test_ignore.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ features = [
"SharedArrayBuffer",
"resizable-arraybuffer",
"Temporal",
"tail-call-optimization",
"ShadowRealm",
"FinalizationRegistry",
"Atomics",
Expand Down